@kodaris/krubble-components 1.0.52 → 1.0.54

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.
Files changed (36) hide show
  1. package/custom-elements.json +3227 -1011
  2. package/dist/button/button.js +1 -1
  3. package/dist/form/auto-suggest/auto-suggest.d.ts +20 -42
  4. package/dist/form/auto-suggest/auto-suggest.d.ts.map +1 -1
  5. package/dist/form/auto-suggest/auto-suggest.js +193 -426
  6. package/dist/form/auto-suggest/auto-suggest.js.map +1 -1
  7. package/dist/form/combo-box/combo-box.d.ts +92 -0
  8. package/dist/form/combo-box/combo-box.d.ts.map +1 -0
  9. package/dist/form/combo-box/combo-box.js +714 -0
  10. package/dist/form/combo-box/combo-box.js.map +1 -0
  11. package/dist/form/index.d.ts +1 -0
  12. package/dist/form/index.d.ts.map +1 -1
  13. package/dist/form/index.js +1 -0
  14. package/dist/form/index.js.map +1 -1
  15. package/dist/form/select-field/select-field.js +1 -1
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/krubble-components.bundled.js +3845 -1477
  21. package/dist/krubble-components.bundled.js.map +1 -1
  22. package/dist/krubble-components.bundled.min.js +1156 -548
  23. package/dist/krubble-components.bundled.min.js.map +1 -1
  24. package/dist/krubble-components.umd.js +5715 -3344
  25. package/dist/krubble-components.umd.js.map +1 -1
  26. package/dist/krubble-components.umd.min.js +1159 -551
  27. package/dist/krubble-components.umd.min.js.map +1 -1
  28. package/dist/table/query.d.ts +63 -0
  29. package/dist/table/query.d.ts.map +1 -0
  30. package/dist/table/query.js +1015 -0
  31. package/dist/table/query.js.map +1 -0
  32. package/dist/table/table.d.ts +63 -10
  33. package/dist/table/table.d.ts.map +1 -1
  34. package/dist/table/table.js +993 -113
  35. package/dist/table/table.js.map +1 -1
  36. package/package.json +5 -1
@@ -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 = [];
109
162
  }
110
- return { ...col };
111
- })
112
- };
113
- this._displayedColumns = this._def.displayedColumns || this._def.columns.map(c => c.id);
163
+ else if (column.filter.type === 'string') {
164
+ column.filter.operator = 'contains';
165
+ }
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,132 @@ 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
+ let left = rect.left;
621
+ if (left + 328 > window.innerWidth) {
622
+ left = window.innerWidth - 328;
623
+ }
624
+ this._filterPanelPos = {
625
+ top: rect.bottom + 4,
626
+ left
627
+ };
628
+ this._filterPanelOpened = column.id;
629
+ if (column.facetable) {
630
+ this._filterPanelTab = 'counts';
631
+ }
632
+ else {
633
+ this._filterPanelTab = 'filter';
634
+ }
635
+ }
636
+ }
637
+ _handleKqlClear(column) {
638
+ column.filter.clear();
639
+ this._page = 1;
640
+ this._fetch();
641
+ }
642
+ _handleFilterClear() {
643
+ const column = this._model.columns.find(c => c.id === this._filterPanelOpened);
644
+ if (column) {
645
+ column.filter.clear();
646
+ if (column.facetable && !column.filterable) {
647
+ column.filter.operator = 'in';
648
+ column.filter.value = [];
649
+ }
650
+ }
651
+ this._filterPanelOpened = null;
652
+ this._page = 1;
653
+ this._fetch();
654
+ }
655
+ _handleFilterTextKeydown(e, column) {
656
+ if (e.key === 'Enter') {
657
+ e.preventDefault();
658
+ this._handleFilterApply();
659
+ }
660
+ }
661
+ _handleOperatorChange(e, column) {
662
+ column.filter.setOperator(e.target.value);
663
+ this.requestUpdate();
664
+ }
665
+ _handleFilterStringChange(e, column) {
666
+ column.filter.setValue(e.target.value);
667
+ this.requestUpdate();
668
+ }
669
+ _handleFilterNumberChange(e, column) {
670
+ column.filter.setValue(Number(e.target.value));
671
+ this.requestUpdate();
672
+ }
673
+ _handleFilterDateChange(e, column) {
674
+ column.filter.setValue(new Date(e.target.value), 'day');
675
+ this.requestUpdate();
676
+ }
677
+ _handleFilterBooleanChange(e, column) {
678
+ column.filter.setValue(e.target.value === 'true');
679
+ this.requestUpdate();
680
+ }
681
+ _handleFilterDateStartChange(e, column) {
682
+ column.filter.setStart(new Date(e.target.value), 'day');
683
+ this.requestUpdate();
684
+ }
685
+ _handleFilterDateEndChange(e, column) {
686
+ column.filter.setEnd(new Date(e.target.value), 'day');
687
+ this.requestUpdate();
688
+ }
689
+ _handleFilterNumberStartChange(e, column) {
690
+ column.filter.setStart(Number(e.target.value));
691
+ this.requestUpdate();
692
+ }
693
+ _handleFilterNumberEndChange(e, column) {
694
+ column.filter.setEnd(Number(e.target.value));
695
+ this.requestUpdate();
696
+ }
697
+ _handleFilterListChange(e, column) {
698
+ const items = e.target.value.split(',').map((v) => v.trim()).filter((v) => v !== '');
699
+ if (column.type === 'number') {
700
+ column.filter.setValue(items.map((v) => Number(v)));
701
+ }
702
+ else {
703
+ column.filter.setValue(items);
704
+ }
705
+ this.requestUpdate();
706
+ }
707
+ _handleFilterApply() {
708
+ this._filterPanelOpened = null;
709
+ this._page = 1;
710
+ this._fetch();
711
+ }
712
+ _handleFilterPanelTabChange(e) {
713
+ this._filterPanelTab = e.detail.activeTabId;
714
+ }
715
+ _handleBucketToggle(e, column, bucket) {
716
+ column.filter.toggle(bucket.val);
717
+ this._page = 1;
718
+ this._fetch();
719
+ }
720
+ // ----------------------------------------------------------------------------
439
721
  // Rendering
440
722
  // ----------------------------------------------------------------------------
441
723
  _renderCellContent(column, row, rowIndex) {
@@ -449,11 +731,10 @@ let KRTable = class KRTable extends LitElement {
449
731
  }
450
732
  switch (column.type) {
451
733
  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);
734
+ if (column.format === 'currency' && typeof value === 'number') {
735
+ return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
736
+ }
737
+ return String(value);
457
738
  case 'date': {
458
739
  let date;
459
740
  if (value instanceof Date) {
@@ -468,10 +749,11 @@ let KRTable = class KRTable extends LitElement {
468
749
  else {
469
750
  date = new Date(value);
470
751
  }
471
- // Show date and time for datetime values
752
+ // Show date and time for datetime values in UTC
472
753
  return date.toLocaleString(undefined, {
473
754
  year: 'numeric', month: 'short', day: 'numeric',
474
- hour: 'numeric', minute: '2-digit'
755
+ hour: 'numeric', minute: '2-digit',
756
+ timeZone: 'UTC'
475
757
  });
476
758
  }
477
759
  case 'boolean':
@@ -588,13 +870,13 @@ let KRTable = class KRTable extends LitElement {
588
870
  * Hidden when there's no title, no actions, and data fits on one page.
589
871
  */
590
872
  _renderHeader() {
591
- if (!this._def.title && !this._def.actions?.length && this._totalPages <= 1) {
873
+ if (!this._model.title && !this._model.actions?.length && this._totalPages <= 1) {
592
874
  return nothing;
593
875
  }
594
876
  return html `
595
877
  <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 `
878
+ <div class="title">${this._model.title ?? ''}</div>
879
+ ${this._model.dataSource?.mode === 'db' && !this._model.columns.some(col => col.searchable) ? html `<div class="search"></div>` : html `
598
880
  <div class="search">
599
881
  <!-- TODO: Saved views dropdown
600
882
  <div class="views">
@@ -624,9 +906,9 @@ let KRTable = class KRTable extends LitElement {
624
906
  <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
907
  </span>
626
908
  <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 `
909
+ ${[...this._model.columns].filter(col => col.type !== 'actions').sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)).map(col => html `
628
910
  <div class="column-picker-item" @click=${() => this._toggleColumn(col.id)}>
629
- <div class="column-picker-checkbox ${this._displayedColumns.includes(col.id) ? 'checked' : ''}">
911
+ <div class="column-picker-checkbox ${this._model.displayedColumns.includes(col.id) ? 'checked' : ''}">
630
912
  <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
913
  </div>
632
914
  <span class="column-picker-label">${col.label ?? col.id}</span>
@@ -634,19 +916,19 @@ let KRTable = class KRTable extends LitElement {
634
916
  `)}
635
917
  </div>
636
918
  </div>
637
- ${this._def.actions?.length === 1 ? html `
919
+ ${this._model.actions?.length === 1 ? html `
638
920
  <kr-button
639
921
  class="actions"
640
- .href=${this._def.actions[0].href}
641
- .target=${this._def.actions[0].target}
642
- @click=${() => this._handleAction(this._def.actions[0])}
922
+ .href=${this._model.actions[0].href}
923
+ .target=${this._model.actions[0].target}
924
+ @click=${() => this._handleAction(this._model.actions[0])}
643
925
  >
644
- ${this._def.actions[0].label}
926
+ ${this._model.actions[0].label}
645
927
  </kr-button>
646
- ` : this._def.actions?.length ? html `
928
+ ` : this._model.actions?.length ? html `
647
929
  <kr-button
648
930
  class="actions"
649
- .options=${this._def.actions.map(a => ({ id: a.id, label: a.label }))}
931
+ .options=${this._model.actions.map(a => ({ id: a.id, label: a.label }))}
650
932
  @option-select=${(e) => this._handleAction({ id: e.detail.id, label: e.detail.label })}
651
933
  >
652
934
  Actions
@@ -669,6 +951,299 @@ let KRTable = class KRTable extends LitElement {
669
951
  }
670
952
  return nothing;
671
953
  }
954
+ _renderFilterPanel() {
955
+ if (!this._filterPanelOpened) {
956
+ return nothing;
957
+ }
958
+ const column = this._model.columns.find(c => c.id === this._filterPanelOpened);
959
+ // Build filter content (operator + value input)
960
+ let valueInput = html ``;
961
+ if (column.filter.operator === 'empty' || column.filter.operator === 'n_empty') {
962
+ valueInput = html `
963
+ <input
964
+ type="text"
965
+ class="filter-panel__input"
966
+ disabled
967
+ .value=${column.filter.text}
968
+ />
969
+ `;
970
+ }
971
+ else if (column.filter.operator === 'between' && column.type === 'date') {
972
+ valueInput = html `
973
+ <input
974
+ type="date"
975
+ class="filter-panel__input"
976
+ .valueAsDate=${column.filter.value?.start ?? null}
977
+ @change=${(e) => this._handleFilterDateStartChange(e, column)}
978
+ />
979
+ <input
980
+ type="date"
981
+ class="filter-panel__input"
982
+ .valueAsDate=${column.filter.value?.end ?? null}
983
+ @change=${(e) => this._handleFilterDateEndChange(e, column)}
984
+ />
985
+ `;
986
+ }
987
+ else if (column.filter.operator === 'between' && column.type === 'number') {
988
+ valueInput = html `
989
+ <input
990
+ type="number"
991
+ class="filter-panel__input"
992
+ placeholder="Start"
993
+ .value=${column.filter.value?.start ?? ''}
994
+ @input=${(e) => this._handleFilterNumberStartChange(e, column)}
995
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
996
+ />
997
+ <input
998
+ type="number"
999
+ class="filter-panel__input"
1000
+ placeholder="End"
1001
+ .value=${column.filter.value?.end ?? ''}
1002
+ @input=${(e) => this._handleFilterNumberEndChange(e, column)}
1003
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1004
+ />
1005
+ `;
1006
+ }
1007
+ else if (column.filter.operator === 'in') {
1008
+ valueInput = html `
1009
+ <textarea
1010
+ class="filter-panel__textarea"
1011
+ rows="3"
1012
+ placeholder="Values (comma-separated)"
1013
+ .value=${column.filter.text}
1014
+ @input=${(e) => this._handleFilterListChange(e, column)}
1015
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1016
+ ></textarea>
1017
+ `;
1018
+ }
1019
+ else if (column.type === 'boolean') {
1020
+ valueInput = html `
1021
+ <kr-select-field
1022
+ placeholder="Value"
1023
+ .value=${String(column.filter.value ?? '')}
1024
+ @change=${(e) => this._handleFilterBooleanChange(e, column)}
1025
+ >
1026
+ <kr-select-option value="true">Yes</kr-select-option>
1027
+ <kr-select-option value="false">No</kr-select-option>
1028
+ </kr-select-field>
1029
+ `;
1030
+ }
1031
+ else if (column.type === 'date') {
1032
+ valueInput = html `
1033
+ <input
1034
+ type="date"
1035
+ class="filter-panel__input"
1036
+ .valueAsDate=${column.filter.value}
1037
+ @change=${(e) => this._handleFilterDateChange(e, column)}
1038
+ />
1039
+ `;
1040
+ }
1041
+ else if (column.type === 'number') {
1042
+ valueInput = html `
1043
+ <input
1044
+ type="number"
1045
+ class="filter-panel__input"
1046
+ placeholder="Value"
1047
+ min="0"
1048
+ .value=${column.filter.text}
1049
+ @input=${(e) => this._handleFilterNumberChange(e, column)}
1050
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1051
+ />
1052
+ `;
1053
+ }
1054
+ else {
1055
+ valueInput = html `
1056
+ <input
1057
+ type="text"
1058
+ class="filter-panel__input"
1059
+ placeholder="Value"
1060
+ .value=${column.filter.text}
1061
+ @input=${(e) => this._handleFilterStringChange(e, column)}
1062
+ @keydown=${(e) => this._handleFilterTextKeydown(e, column)}
1063
+ />
1064
+ `;
1065
+ }
1066
+ const filterContent = html `
1067
+ <div class="filter-panel__content">
1068
+ <kr-select-field
1069
+ .value=${column.filter.operator}
1070
+ @change=${(e) => this._handleOperatorChange(e, column)}
1071
+ >
1072
+ ${getOperatorsForType(column.type).map(op => html `
1073
+ <kr-select-option value=${op.key}>${op.label}</kr-select-option>
1074
+ `)}
1075
+ </kr-select-field>
1076
+ ${valueInput}
1077
+ </div>
1078
+ `;
1079
+ // Build bucket list content
1080
+ const buckets = this._buckets.get(column.id) || [];
1081
+ let bucketContent;
1082
+ if (!buckets.length) {
1083
+ bucketContent = html `<div class="bucket-empty">No data</div>`;
1084
+ }
1085
+ else {
1086
+ bucketContent = html `
1087
+ <div class="buckets">
1088
+ ${buckets.map(bucket => {
1089
+ let bucketLabel = '(Empty)';
1090
+ if (bucket.val !== null && bucket.val !== undefined) {
1091
+ if (column.type === 'boolean') {
1092
+ if (bucket.val === true || bucket.val === 'true') {
1093
+ bucketLabel = 'Yes';
1094
+ }
1095
+ else {
1096
+ bucketLabel = 'No';
1097
+ }
1098
+ }
1099
+ else {
1100
+ bucketLabel = String(bucket.val);
1101
+ }
1102
+ }
1103
+ let checkIcon = nothing;
1104
+ if (column.filter.has(bucket.val)) {
1105
+ checkIcon = html `
1106
+ <svg viewBox="0 0 24 24" fill="currentColor">
1107
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
1108
+ </svg>
1109
+ `;
1110
+ }
1111
+ return html `
1112
+ <div
1113
+ class="bucket"
1114
+ @click=${(e) => this._handleBucketToggle(e, column, bucket)}
1115
+ >
1116
+ <div class=${classMap({
1117
+ 'bucket__checkbox': true,
1118
+ 'bucket__checkbox--checked': column.filter.has(bucket.val)
1119
+ })}>
1120
+ ${checkIcon}
1121
+ </div>
1122
+ <span class="bucket__label">${bucketLabel}</span>
1123
+ <span class="bucket__count">${bucket.count}</span>
1124
+ </div>
1125
+ `;
1126
+ })}
1127
+ </div>
1128
+ `;
1129
+ }
1130
+ // Build panel body — tabs if both filterable+facetable, otherwise just the relevant content
1131
+ let panelBody;
1132
+ if (column.facetable && column.filterable) {
1133
+ panelBody = html `
1134
+ <kr-tab-group
1135
+ size="small"
1136
+ active-tab-id=${this._filterPanelTab}
1137
+ @tab-change=${(e) => this._handleFilterPanelTabChange(e)}
1138
+ >
1139
+ <kr-tab id="filter" label="Filter">
1140
+ ${filterContent}
1141
+ </kr-tab>
1142
+ <kr-tab id="counts" label="Counts">
1143
+ ${bucketContent}
1144
+ </kr-tab>
1145
+ </kr-tab-group>
1146
+ `;
1147
+ }
1148
+ else if (column.facetable) {
1149
+ panelBody = bucketContent;
1150
+ }
1151
+ else {
1152
+ panelBody = filterContent;
1153
+ }
1154
+ return html `
1155
+ <div
1156
+ class="filter-panel"
1157
+ style=${styleMap({
1158
+ top: this._filterPanelPos.top + 'px',
1159
+ left: this._filterPanelPos.left + 'px'
1160
+ })}
1161
+ >
1162
+ ${panelBody}
1163
+ <div class="filter-panel__actions">
1164
+ <kr-button variant="outline" color="secondary" size="small" @click=${this._handleFilterClear}>
1165
+ Clear
1166
+ </kr-button>
1167
+ <kr-button size="small" @click=${this._handleFilterApply}>
1168
+ Apply
1169
+ </kr-button>
1170
+ </div>
1171
+ </div>
1172
+ `;
1173
+ }
1174
+ /**
1175
+ * Renders filter row below column headers.
1176
+ * Only displays for columns with filterable: true.
1177
+ */
1178
+ _renderFilterRow() {
1179
+ const columns = this.getDisplayedColumns();
1180
+ if (!columns.some(col => col.filterable || col.facetable)) {
1181
+ return nothing;
1182
+ }
1183
+ return html `
1184
+ <div class="filter-row">
1185
+ ${columns.map((col, i) => {
1186
+ if (!col.filterable && !col.facetable) {
1187
+ return html `<div
1188
+ class=${classMap({
1189
+ 'filter-cell': true,
1190
+ 'filter-cell--sticky-left': col.sticky === 'left',
1191
+ 'filter-cell--sticky-right': col.sticky === 'right',
1192
+ 'filter-cell--sticky-right-first': col.sticky === 'right' &&
1193
+ !columns.slice(0, i).some((c) => c.sticky === 'right')
1194
+ })}
1195
+ style=${styleMap(this._getCellStyle(col, i))}
1196
+ ></div>`;
1197
+ }
1198
+ return html `
1199
+ <div
1200
+ class=${classMap({
1201
+ 'filter-cell': true,
1202
+ 'filter-cell--sticky-left': col.sticky === 'left',
1203
+ 'filter-cell--sticky-right': col.sticky === 'right',
1204
+ 'filter-cell--sticky-right-first': col.sticky === 'right' &&
1205
+ !columns.slice(0, i).some((c) => c.sticky === 'right')
1206
+ })}
1207
+ style=${styleMap(this._getCellStyle(col, i))}
1208
+ >
1209
+ <div class="filter-cell__wrapper">
1210
+ <input
1211
+ type="text"
1212
+ class=${classMap({
1213
+ 'filter-cell__input': true,
1214
+ 'filter-cell__input--invalid': !col.filter.isValid()
1215
+ })}
1216
+ .value=${col.filter.kql}
1217
+ @change=${(e) => this._handleKqlChange(e, col)}
1218
+ />
1219
+ ${col.filter?.kql?.length > 0 ? html `
1220
+ <button
1221
+ class="filter-cell__clear"
1222
+ @click=${() => this._handleKqlClear(col)}
1223
+ >
1224
+ <svg viewBox="0 0 24 24" fill="currentColor">
1225
+ <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"/>
1226
+ </svg>
1227
+ </button>
1228
+ ` : nothing}
1229
+ <button
1230
+ class=${classMap({
1231
+ 'filter-cell__advanced': true,
1232
+ 'filter-cell__advanced--opened': this._filterPanelOpened === col.id
1233
+ })}
1234
+ @click=${(e) => this._handleFilterPanelToggle(e, col)}
1235
+ >
1236
+ <svg viewBox="0 0 24 24" fill="currentColor">
1237
+ <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
1238
+ </svg>
1239
+ </button>
1240
+ </div>
1241
+ </div>
1242
+ `;
1243
+ })}
1244
+ </div>
1245
+ `;
1246
+ }
672
1247
  /** Renders the scrollable data grid with column headers and rows. */
673
1248
  _renderTable() {
674
1249
  return html `
@@ -690,6 +1265,7 @@ let KRTable = class KRTable extends LitElement {
690
1265
  ></div>` : nothing}</div>
691
1266
  `)}
692
1267
  </div>
1268
+ ${this._renderFilterRow()}
693
1269
  ${this._data.map((row, rowIndex) => {
694
1270
  const cells = this.getDisplayedColumns().map((col, i) => html `
695
1271
  <div
@@ -700,10 +1276,10 @@ let KRTable = class KRTable extends LitElement {
700
1276
  ${this._renderCellContent(col, row, rowIndex)}
701
1277
  </div>
702
1278
  `);
703
- if (this._def.rowHref) {
1279
+ if (this._model.rowHref) {
704
1280
  return html `
705
1281
  <a
706
- href=${this._def.rowHref(row)}
1282
+ href=${this._model.rowHref(row)}
707
1283
  class=${classMap({ 'row': true, 'row--clickable': true, 'row--link': true })}
708
1284
  @mousedown=${() => this._handleRowMouseDown()}
709
1285
  @click=${() => this._handleRowClick(row, rowIndex)}
@@ -712,7 +1288,7 @@ let KRTable = class KRTable extends LitElement {
712
1288
  }
713
1289
  return html `
714
1290
  <div
715
- class=${classMap({ 'row': true, 'row--clickable': !!this._def.rowClickable })}
1291
+ class=${classMap({ 'row': true, 'row--clickable': !!this._model.rowClickable })}
716
1292
  @mousedown=${() => this._handleRowMouseDown()}
717
1293
  @click=${() => this._handleRowClick(row, rowIndex)}
718
1294
  >${cells}</div>
@@ -730,12 +1306,13 @@ let KRTable = class KRTable extends LitElement {
730
1306
  * - Loading, error message, or empty state when no data
731
1307
  */
732
1308
  render() {
733
- if (!this._def.columns.length) {
1309
+ if (!this._model.columns.length) {
734
1310
  return html `<slot></slot>`;
735
1311
  }
736
1312
  return html `
737
1313
  ${this._renderHeader()}
738
1314
  ${this._renderTable()}
1315
+ ${this._renderFilterPanel()}
739
1316
  `;
740
1317
  }
741
1318
  };
@@ -1084,6 +1661,7 @@ KRTable.styles = [krBaseCSS, css `
1084
1661
  overflow: hidden;
1085
1662
  text-overflow: ellipsis;
1086
1663
  box-sizing: border-box;
1664
+ border-right: 1px solid #e5e7ebba;
1087
1665
  }
1088
1666
 
1089
1667
  .cell--actions {
@@ -1100,7 +1678,9 @@ KRTable.styles = [krBaseCSS, css `
1100
1678
  white-space: nowrap;
1101
1679
  box-sizing: border-box;
1102
1680
  background: #f9fafb;
1681
+ border-top: 1px solid #e5e7eb;
1103
1682
  border-bottom: 2px solid #e5e7eb;
1683
+ border-right: 1px solid #e5e7ebba;
1104
1684
  font-weight: 600;
1105
1685
  color: #374151;
1106
1686
  overflow: hidden;
@@ -1198,7 +1778,6 @@ KRTable.styles = [krBaseCSS, css `
1198
1778
  * ----------------------------------------------------------------------- */
1199
1779
  :host(.kr-table--scroll-overlay) .content {
1200
1780
  padding-left: 24px;
1201
- padding-right: 24px;
1202
1781
  }
1203
1782
 
1204
1783
  .overlay-left,
@@ -1257,6 +1836,301 @@ KRTable.styles = [krBaseCSS, css `
1257
1836
  .status--error {
1258
1837
  color: #dc2626;
1259
1838
  }
1839
+
1840
+ /* -------------------------------------------------------------------------
1841
+ * Filter Row
1842
+ * ----------------------------------------------------------------------- */
1843
+ .filter-row {
1844
+ display: contents;
1845
+ }
1846
+
1847
+ .filter-cell {
1848
+ position: sticky;
1849
+ top: 48px;
1850
+ z-index: 2;
1851
+ height: 40px;
1852
+ padding: 4px 8px;
1853
+ display: flex;
1854
+ align-items: center;
1855
+ background: #fafbfc;
1856
+ border-bottom: 1px solid #e5e7eb;
1857
+ border-right: 1px solid #e5e7ebba;
1858
+ box-sizing: border-box;
1859
+ }
1860
+
1861
+
1862
+ .filter-cell--sticky-left,
1863
+ .filter-cell--sticky-right {
1864
+ position: sticky;
1865
+ z-index: 3;
1866
+ }
1867
+
1868
+ .filter-cell--sticky-right-first {
1869
+ border-left: 1px solid #d1d5db;
1870
+ }
1871
+
1872
+ .filter-cell__wrapper {
1873
+ position: relative;
1874
+ display: flex;
1875
+ align-items: center;
1876
+ gap: 4px;
1877
+ width: 100%;
1878
+ }
1879
+
1880
+ .filter-cell__input {
1881
+ width: 100%;
1882
+ height: 32px;
1883
+ padding: 0 52px 0 8px;
1884
+ border: 1px solid #d1d5db;
1885
+ border-radius: 6px;
1886
+ font-size: 14px;
1887
+ font-family: inherit;
1888
+ color: #111827;
1889
+ background: #fff;
1890
+ outline: none;
1891
+ transition: border-color 0.2s, box-shadow 0.2s;
1892
+ }
1893
+
1894
+ .filter-cell__input:focus {
1895
+ border-color: #163052;
1896
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
1897
+ }
1898
+
1899
+ .filter-cell__input--invalid {
1900
+ border-color: #dc2626;
1901
+ }
1902
+
1903
+ .filter-cell__input--invalid:focus {
1904
+ border-color: #dc2626;
1905
+ box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.1);
1906
+ }
1907
+
1908
+ .filter-cell__input::placeholder {
1909
+ color: #9ca3af;
1910
+ font-size: 13px;
1911
+ }
1912
+
1913
+ .filter-cell__clear {
1914
+ position: absolute;
1915
+ right: 28px;
1916
+ top: 50%;
1917
+ transform: translateY(-50%);
1918
+ display: flex;
1919
+ align-items: center;
1920
+ justify-content: center;
1921
+ width: 24px;
1922
+ height: 24px;
1923
+ padding: 0;
1924
+ border: none;
1925
+ border-radius: 4px;
1926
+ background: transparent;
1927
+ color: #6b7280;
1928
+ cursor: pointer;
1929
+ transition: background 0.15s, color 0.15s;
1930
+ }
1931
+
1932
+ .filter-cell__clear:hover {
1933
+ background: #e5e7eb;
1934
+ color: #374151;
1935
+ }
1936
+
1937
+ .filter-cell__clear svg {
1938
+ width: 16px;
1939
+ height: 16px;
1940
+ }
1941
+
1942
+ .filter-cell__advanced {
1943
+ position: absolute;
1944
+ right: 4px;
1945
+ top: 50%;
1946
+ transform: translateY(-50%);
1947
+ display: flex;
1948
+ align-items: center;
1949
+ justify-content: center;
1950
+ width: 24px;
1951
+ height: 24px;
1952
+ padding: 0;
1953
+ border: none;
1954
+ border-radius: 4px;
1955
+ background: transparent;
1956
+ color: #163052;
1957
+ cursor: pointer;
1958
+ transition: background 0.15s, color 0.15s;
1959
+ }
1960
+
1961
+ .filter-cell__advanced:hover {
1962
+ background: #e5e7eb;
1963
+ }
1964
+
1965
+ .filter-cell__advanced svg {
1966
+ width: 16px;
1967
+ height: 16px;
1968
+ }
1969
+
1970
+ .filter-cell__advanced--opened {
1971
+ background: #163052;
1972
+ color: #fff;
1973
+ }
1974
+
1975
+ .filter-cell__advanced--opened:hover {
1976
+ background: #1a3a5f;
1977
+ color: #fff;
1978
+ }
1979
+
1980
+ /* -------------------------------------------------------------------------
1981
+ * Filter Panel (Advanced)
1982
+ * ----------------------------------------------------------------------- */
1983
+ .filter-panel {
1984
+ position: fixed;
1985
+ min-width: 320px;
1986
+ background: white;
1987
+ border: 1px solid #9ba7b6;
1988
+ border-radius: 8px;
1989
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
1990
+ z-index: 100;
1991
+ animation: filter-panel-fade-in 150ms ease-out;
1992
+ }
1993
+
1994
+ @keyframes filter-panel-fade-in {
1995
+ from {
1996
+ opacity: 0;
1997
+ transform: translateY(-4px);
1998
+ }
1999
+ to {
2000
+ opacity: 1;
2001
+ transform: translateY(0);
2002
+ }
2003
+ }
2004
+
2005
+ .filter-panel__content {
2006
+ padding: 16px;
2007
+ display: flex;
2008
+ flex-direction: column;
2009
+ gap: 12px;
2010
+ }
2011
+
2012
+ .filter-panel__actions {
2013
+ padding: 12px 16px;
2014
+ border-top: 1px solid #e5e7eb;
2015
+ display: flex;
2016
+ justify-content: flex-end;
2017
+ align-items: center;
2018
+ gap: 12px;
2019
+ }
2020
+
2021
+
2022
+ .filter-panel__input {
2023
+ width: 100%;
2024
+ padding: 10px 12px;
2025
+ border: 1px solid #d1d5db;
2026
+ border-radius: 8px;
2027
+ font-size: 14px;
2028
+ font-family: inherit;
2029
+ color: #111827;
2030
+ background: #fff;
2031
+ outline: none;
2032
+ transition: border-color 0.2s, box-shadow 0.2s;
2033
+ }
2034
+
2035
+ .filter-panel__input:focus {
2036
+ border-color: #163052;
2037
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
2038
+ }
2039
+
2040
+ .filter-panel__input::placeholder {
2041
+ color: #9ca3af;
2042
+ }
2043
+
2044
+ .filter-panel__textarea {
2045
+ width: 100%;
2046
+ padding: 10px 12px;
2047
+ border: 1px solid #d1d5db;
2048
+ border-radius: 8px;
2049
+ font-size: 14px;
2050
+ font-family: inherit;
2051
+ color: #111827;
2052
+ background: #fff;
2053
+ outline: none;
2054
+ resize: vertical;
2055
+ transition: border-color 0.2s, box-shadow 0.2s;
2056
+ }
2057
+
2058
+ .filter-panel__textarea:focus {
2059
+ border-color: #163052;
2060
+ box-shadow: 0 0 0 2px rgba(22, 48, 82, 0.1);
2061
+ }
2062
+
2063
+ .filter-panel__textarea::placeholder {
2064
+ color: #9ca3af;
2065
+ }
2066
+
2067
+ /* -------------------------------------------------------------------------
2068
+ * Bucket List
2069
+ * ----------------------------------------------------------------------- */
2070
+ .buckets {
2071
+ max-height: 280px;
2072
+ overflow-y: auto;
2073
+ padding: 8px 0;
2074
+ }
2075
+
2076
+ .bucket {
2077
+ display: flex;
2078
+ align-items: center;
2079
+ gap: 16px;
2080
+ height: 32px;
2081
+ padding: 0 16px;
2082
+ cursor: pointer;
2083
+ transition: background 0.1s;
2084
+ }
2085
+
2086
+ .bucket:hover {
2087
+ background: #f3f4f6;
2088
+ }
2089
+
2090
+ .bucket__checkbox {
2091
+ width: 16px;
2092
+ height: 16px;
2093
+ border: 1.5px solid #9ca3af;
2094
+ border-radius: 3px;
2095
+ display: flex;
2096
+ align-items: center;
2097
+ justify-content: center;
2098
+ flex-shrink: 0;
2099
+ transition: all 0.15s;
2100
+ }
2101
+
2102
+ .bucket__checkbox--checked {
2103
+ background: var(--kr-primary, #163052);
2104
+ border-color: var(--kr-primary, #163052);
2105
+ }
2106
+
2107
+ .bucket__checkbox svg {
2108
+ width: 12px;
2109
+ height: 12px;
2110
+ color: white;
2111
+ }
2112
+
2113
+ .bucket__label {
2114
+ flex: 1;
2115
+ font-size: 14px;
2116
+ color: #000;
2117
+ overflow: hidden;
2118
+ text-overflow: ellipsis;
2119
+ white-space: nowrap;
2120
+ }
2121
+
2122
+ .bucket__count {
2123
+ font-size: 14px;
2124
+ color: #000;
2125
+ flex-shrink: 0;
2126
+ }
2127
+
2128
+ .bucket-empty {
2129
+ font-size: 14px;
2130
+ color: #000;
2131
+ padding: 16px;
2132
+ }
2133
+
1260
2134
  `];
1261
2135
  __decorate([
1262
2136
  state()
@@ -1293,7 +2167,13 @@ __decorate([
1293
2167
  ], KRTable.prototype, "_columnPickerOpen", void 0);
1294
2168
  __decorate([
1295
2169
  state()
1296
- ], KRTable.prototype, "_displayedColumns", void 0);
2170
+ ], KRTable.prototype, "_filterPanelOpened", void 0);
2171
+ __decorate([
2172
+ state()
2173
+ ], KRTable.prototype, "_filterPanelTab", void 0);
2174
+ __decorate([
2175
+ state()
2176
+ ], KRTable.prototype, "_buckets", void 0);
1297
2177
  __decorate([
1298
2178
  property({ type: Object })
1299
2179
  ], KRTable.prototype, "def", void 0);