@gradio/dataframe 0.17.16 → 0.18.0

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 (34) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/Dataframe.stories.svelte +1 -2
  3. package/Index.svelte +3 -13
  4. package/dist/Index.svelte +3 -10
  5. package/dist/Index.svelte.d.ts +1 -5
  6. package/dist/shared/CellMenu.svelte +37 -0
  7. package/dist/shared/CellMenu.svelte.d.ts +4 -1
  8. package/dist/shared/CellMenuIcons.svelte +48 -0
  9. package/dist/shared/EditableCell.svelte +0 -2
  10. package/dist/shared/EditableCell.svelte.d.ts +0 -1
  11. package/dist/shared/FilterMenu.svelte +235 -0
  12. package/dist/shared/FilterMenu.svelte.d.ts +19 -0
  13. package/dist/shared/Table.svelte +59 -5
  14. package/dist/shared/TableCell.svelte +0 -2
  15. package/dist/shared/TableCell.svelte.d.ts +0 -1
  16. package/dist/shared/TableHeader.svelte +26 -2
  17. package/dist/shared/TableHeader.svelte.d.ts +7 -1
  18. package/dist/shared/context/dataframe_context.d.ts +19 -0
  19. package/dist/shared/context/dataframe_context.js +46 -6
  20. package/dist/shared/utils/filter_utils.d.ts +28 -0
  21. package/dist/shared/utils/filter_utils.js +123 -0
  22. package/dist/shared/utils/table_utils.d.ts +7 -0
  23. package/dist/shared/utils/table_utils.js +29 -0
  24. package/package.json +9 -9
  25. package/shared/CellMenu.svelte +45 -1
  26. package/shared/CellMenuIcons.svelte +48 -0
  27. package/shared/EditableCell.svelte +0 -2
  28. package/shared/FilterMenu.svelte +248 -0
  29. package/shared/Table.svelte +78 -8
  30. package/shared/TableCell.svelte +0 -2
  31. package/shared/TableHeader.svelte +31 -2
  32. package/shared/context/dataframe_context.ts +80 -17
  33. package/shared/utils/filter_utils.ts +207 -0
  34. package/shared/utils/table_utils.ts +52 -0
@@ -1,8 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import CellMenuIcons from "./CellMenuIcons.svelte";
4
+ import FilterMenu from "./FilterMenu.svelte";
4
5
  import type { I18nFormatter } from "js/utils/src";
5
- import type { SortDirection } from "./context/dataframe_context";
6
+ import type {
7
+ SortDirection,
8
+ FilterDatatype
9
+ } from "./context/dataframe_context";
6
10
 
7
11
  export let x: number;
8
12
  export let y: number;
@@ -21,10 +25,18 @@
21
25
  export let on_clear_sort: () => void = () => {};
22
26
  export let sort_direction: SortDirection | null = null;
23
27
  export let sort_priority: number | null = null;
28
+ export let on_filter: (
29
+ datatype: FilterDatatype,
30
+ selected_filter: string,
31
+ value: string
32
+ ) => void = () => {};
33
+ export let on_clear_filter: () => void = () => {};
34
+ export let filter_active: boolean | null = null;
24
35
  export let editable = true;
25
36
 
26
37
  export let i18n: I18nFormatter;
27
38
  let menu_element: HTMLDivElement;
39
+ let active_filter_menu: { x: number; y: number } | null = null;
28
40
 
29
41
  $: is_header = row === -1;
30
42
  $: can_add_rows = editable && row_count[1] === "dynamic";
@@ -55,6 +67,19 @@
55
67
  menu_element.style.left = `${new_x}px`;
56
68
  menu_element.style.top = `${new_y}px`;
57
69
  }
70
+
71
+ function toggle_filter_menu(): void {
72
+ if (filter_active) {
73
+ on_filter("string", "", "");
74
+ return;
75
+ }
76
+
77
+ const menu_rect = menu_element.getBoundingClientRect();
78
+ active_filter_menu = {
79
+ x: menu_rect.right,
80
+ y: menu_rect.top + menu_rect.height / 2
81
+ };
82
+ }
58
83
  </script>
59
84
 
60
85
  <div bind:this={menu_element} class="cell-menu" role="menu">
@@ -85,6 +110,21 @@
85
110
  <CellMenuIcons icon="clear-sort" />
86
111
  {i18n("dataframe.clear_sort")}
87
112
  </button>
113
+ <button
114
+ role="menuitem"
115
+ on:click|stopPropagation={toggle_filter_menu}
116
+ class:active={filter_active || active_filter_menu}
117
+ >
118
+ <CellMenuIcons icon="filter" />
119
+ {i18n("dataframe.filter")}
120
+ {#if filter_active}
121
+ <span class="priority">1</span>
122
+ {/if}
123
+ </button>
124
+ <button role="menuitem" on:click={on_clear_filter}>
125
+ <CellMenuIcons icon="clear-filter" />
126
+ {i18n("dataframe.clear_filter")}
127
+ </button>
88
128
  {/if}
89
129
 
90
130
  {#if !is_header && can_add_rows}
@@ -147,6 +187,10 @@
147
187
  {/if}
148
188
  </div>
149
189
 
190
+ {#if active_filter_menu}
191
+ <FilterMenu {on_filter} />
192
+ {/if}
193
+
150
194
  <style>
151
195
  .cell-menu {
152
196
  position: fixed;
@@ -189,4 +189,52 @@
189
189
  stroke-linecap="round"
190
190
  />
191
191
  </svg>
192
+ {:else if icon == "filter"}
193
+ <svg viewBox="0 0 24 24" width="16" height="16">
194
+ <path
195
+ d="M5 5H19"
196
+ stroke="currentColor"
197
+ stroke-width="2"
198
+ stroke-linecap="round"
199
+ />
200
+ <path
201
+ d="M8 9H16"
202
+ stroke="currentColor"
203
+ stroke-width="2"
204
+ stroke-linecap="round"
205
+ />
206
+ <path
207
+ d="M11 13H13"
208
+ stroke="currentColor"
209
+ stroke-width="2"
210
+ stroke-linecap="round"
211
+ />
212
+ </svg>
213
+ {:else if icon == "clear-filter"}
214
+ <svg viewBox="0 0 24 24" width="16" height="16">
215
+ <path
216
+ d="M5 5H19"
217
+ stroke="currentColor"
218
+ stroke-width="2"
219
+ stroke-linecap="round"
220
+ />
221
+ <path
222
+ d="M8 9H16"
223
+ stroke="currentColor"
224
+ stroke-width="2"
225
+ stroke-linecap="round"
226
+ />
227
+ <path
228
+ d="M11 13H13"
229
+ stroke="currentColor"
230
+ stroke-width="2"
231
+ stroke-linecap="round"
232
+ />
233
+ <path
234
+ d="M17 17L21 21M21 17L17 21"
235
+ stroke="currentColor"
236
+ stroke-width="2"
237
+ stroke-linecap="round"
238
+ />
239
+ </svg>
192
240
  {/if}
@@ -26,7 +26,6 @@
26
26
  export let line_breaks = true;
27
27
  export let editable = true;
28
28
  export let is_static = false;
29
- export let root: string;
30
29
  export let max_chars: number | null = null;
31
30
  export let components: Record<string, any> = {};
32
31
  export let i18n: I18nFormatter;
@@ -178,7 +177,6 @@
178
177
  {latex_delimiters}
179
178
  {line_breaks}
180
179
  chatbot={false}
181
- {root}
182
180
  />
183
181
  {:else}
184
182
  {display_text}
@@ -0,0 +1,248 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { Check } from "@gradio/icons";
4
+ import DropdownArrow from "../../icons/src/DropdownArrow.svelte";
5
+ import type { FilterDatatype } from "./context/dataframe_context";
6
+
7
+ export let on_filter: (
8
+ datatype: FilterDatatype,
9
+ selected_filter: string,
10
+ value: string
11
+ ) => void = () => {};
12
+
13
+ let menu_element: HTMLDivElement;
14
+ let datatype: "string" | "number" = "string";
15
+ let current_filter = "Contains";
16
+ let filter_dropdown_open = false;
17
+ let filter_input_value = "";
18
+
19
+ const filter_options = {
20
+ string: [
21
+ "Contains",
22
+ "Does not contain",
23
+ "Starts with",
24
+ "Ends with",
25
+ "Is",
26
+ "Is not",
27
+ "Is empty",
28
+ "Is not empty"
29
+ ],
30
+ number: ["=", "≠", ">", "<", "≥", "≤", "Is empty", "Is not empty"]
31
+ };
32
+
33
+ onMount(() => {
34
+ position_menu();
35
+ });
36
+
37
+ function position_menu(): void {
38
+ if (!menu_element) return;
39
+
40
+ const viewport_width = window.innerWidth;
41
+ const viewport_height = window.innerHeight;
42
+ const menu_rect = menu_element.getBoundingClientRect();
43
+
44
+ const x = (viewport_width - menu_rect.width) / 2;
45
+ const y = (viewport_height - menu_rect.height) / 2;
46
+
47
+ menu_element.style.left = `${x}px`;
48
+ menu_element.style.top = `${y}px`;
49
+ }
50
+
51
+ function handle_filter_input(e: Event): void {
52
+ const target = e.target as HTMLInputElement;
53
+ filter_input_value = target.value;
54
+ }
55
+ </script>
56
+
57
+ <div>
58
+ <div class="background"></div>
59
+ <div bind:this={menu_element} class="filter-menu">
60
+ <div class="filter-datatype-container">
61
+ <span>Filter as</span>
62
+ <button
63
+ on:click|stopPropagation={() => {
64
+ datatype = datatype === "string" ? "number" : "string";
65
+ current_filter = filter_options[datatype][0];
66
+ }}
67
+ aria-label={`Change filter type. Filtering ${datatype}s`}
68
+ >
69
+ {datatype}
70
+ </button>
71
+ </div>
72
+
73
+ <div class="input-container">
74
+ <div class="filter-dropdown">
75
+ <button
76
+ on:click|stopPropagation={() =>
77
+ (filter_dropdown_open = !filter_dropdown_open)}
78
+ aria-label={`Change filter. Using '${current_filter}'`}
79
+ >
80
+ {current_filter}
81
+ <DropdownArrow />
82
+ </button>
83
+
84
+ {#if filter_dropdown_open}
85
+ <div class="dropdown-filter-options">
86
+ {#each filter_options[datatype] as opt}
87
+ <button
88
+ on:click|stopPropagation={() => {
89
+ current_filter = opt;
90
+ filter_dropdown_open = !filter_dropdown_open;
91
+ }}
92
+ class="filter-option"
93
+ >
94
+ {opt}
95
+ </button>
96
+ {/each}
97
+ </div>
98
+ {/if}
99
+ </div>
100
+
101
+ <input
102
+ type="text"
103
+ value={filter_input_value}
104
+ on:click|stopPropagation
105
+ on:input={handle_filter_input}
106
+ placeholder="Type a value"
107
+ class="filter-input"
108
+ />
109
+ </div>
110
+
111
+ <button
112
+ class="check-button"
113
+ on:click={() => on_filter(datatype, current_filter, filter_input_value)}
114
+ >
115
+ <Check />
116
+ </button>
117
+ </div>
118
+ </div>
119
+
120
+ <style>
121
+ .background {
122
+ position: fixed;
123
+ top: 0;
124
+ left: 0;
125
+ width: 100vw;
126
+ height: 100vh;
127
+ background-color: rgba(0, 0, 0, 0.4);
128
+ z-index: 20;
129
+ }
130
+
131
+ .filter-menu {
132
+ position: fixed;
133
+ background: var(--background-fill-primary);
134
+ border: 1px solid var(--border-color-primary);
135
+ border-radius: var(--radius-sm);
136
+ padding: var(--size-2);
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: var(--size-2);
140
+ box-shadow: var(--shadow-drop-lg);
141
+ width: 300px;
142
+ z-index: 21;
143
+ }
144
+
145
+ .filter-datatype-container {
146
+ display: flex;
147
+ gap: var(--size-2);
148
+ align-items: center;
149
+ }
150
+
151
+ .filter-menu span {
152
+ font-size: var(--text-sm);
153
+ color: var(--body-text-color);
154
+ }
155
+
156
+ .filter-menu button {
157
+ height: var(--size-6);
158
+ background: none;
159
+ border: 1px solid var(--border-color-primary);
160
+ border-radius: var(--radius-sm);
161
+ padding: var(--size-1) var(--size-2);
162
+ color: var(--body-text-color);
163
+ font-size: var(--text-sm);
164
+ cursor: pointer;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ gap: var(--size-2);
169
+ }
170
+
171
+ .filter-menu button:hover {
172
+ background-color: var(--background-fill-secondary);
173
+ }
174
+
175
+ .filter-input {
176
+ width: var(--size-full);
177
+ height: var(--size-6);
178
+ padding: var(--size-2);
179
+ padding-right: var(--size-8);
180
+ border: 1px solid var(--border-color-primary);
181
+ border-radius: var(--table-radius);
182
+ font-size: var(--text-sm);
183
+ color: var(--body-text-color);
184
+ background: var(--background-fill-secondary);
185
+ transition: all 0.2s ease;
186
+ }
187
+
188
+ .filter-input:hover {
189
+ border-color: var(--border-color-secondary);
190
+ background: var(--background-fill-primary);
191
+ }
192
+
193
+ .filter-input:focus {
194
+ outline: none;
195
+ border-color: var(--color-accent);
196
+ background: var(--background-fill-primary);
197
+ box-shadow: 0 0 0 1px var(--color-accent);
198
+ }
199
+
200
+ .dropdown-filter-options {
201
+ display: flex;
202
+ flex-direction: column;
203
+ background: var(--background-fill-primary);
204
+ border: 1px solid var(--border-color-primary);
205
+ border-radius: var(--radius-sm);
206
+ box-shadow: var(--shadow-drop-md);
207
+ position: absolute;
208
+ z-index: var(--layer-1);
209
+ }
210
+
211
+ .dropdown-filter-options .filter-option {
212
+ border: none;
213
+ justify-content: flex-start;
214
+ }
215
+
216
+ .input-container {
217
+ display: flex;
218
+ gap: var(--size-2);
219
+ align-items: center;
220
+ }
221
+
222
+ .input-container button {
223
+ width: 130px;
224
+ }
225
+
226
+ :global(svg.dropdown-arrow) {
227
+ width: var(--size-4);
228
+ height: var(--size-4);
229
+ margin-left: auto;
230
+ }
231
+
232
+ .filter-menu .check-button {
233
+ background: var(--color-accent);
234
+ color: white;
235
+ border: none;
236
+ width: var(--size-full);
237
+ height: var(--size-6);
238
+ border-radius: var(--radius-sm);
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ padding: var(--size-1);
243
+ }
244
+
245
+ .check-button:hover {
246
+ background: var(--color-accent-soft);
247
+ }
248
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" context="module">
2
2
  import {
3
3
  create_dataframe_context,
4
- type SortDirection
4
+ type SortDirection,
5
+ type FilterDatatype
5
6
  } from "./context/dataframe_context";
6
7
  </script>
7
8
 
@@ -43,6 +44,7 @@
43
44
  type DragHandlers
44
45
  } from "./utils/drag_utils";
45
46
  import { sort_data_and_preserve_selection } from "./utils/sort_utils";
47
+ import { filter_data_and_preserve_selection } from "./utils/filter_utils";
46
48
 
47
49
  export let datatype: Datatype | Datatype[];
48
50
  export let label: string | null = null;
@@ -261,6 +263,12 @@
261
263
  df_actions.reset_sort_state();
262
264
  }
263
265
 
266
+ if ($df_state.filter_state.filter_columns.length > 0) {
267
+ filter_data(data, display_value, styling);
268
+ } else {
269
+ df_actions.reset_filter_state();
270
+ }
271
+
264
272
  if ($df_state.current_search_query) {
265
273
  df_actions.handle_search(null);
266
274
  }
@@ -340,11 +348,33 @@
340
348
 
341
349
  function clear_sort(): void {
342
350
  df_actions.reset_sort_state();
351
+ sort_data(data, display_value, styling);
343
352
  }
344
353
 
345
- $: if ($df_state.sort_state.sort_columns.length > 0) {
346
- sort_data(data, display_value, styling);
347
- df_actions.update_row_order(data);
354
+ $: {
355
+ if ($df_state.filter_state.filter_columns.length > 0) {
356
+ filter_data(data, display_value, styling);
357
+ }
358
+
359
+ if ($df_state.sort_state.sort_columns.length > 0) {
360
+ sort_data(data, display_value, styling);
361
+ df_actions.update_row_order(data);
362
+ }
363
+ }
364
+
365
+ function handle_filter(
366
+ col: number,
367
+ datatype: FilterDatatype,
368
+ filter: string,
369
+ value: string
370
+ ): void {
371
+ df_actions.handle_filter(col, datatype, filter, value);
372
+ filter_data(data, display_value, styling);
373
+ }
374
+
375
+ function clear_filter(): void {
376
+ df_actions.reset_filter_state();
377
+ filter_data(data, display_value, styling);
348
378
  }
349
379
 
350
380
  async function edit_header(i: number, _select = false): Promise<void> {
@@ -449,6 +479,9 @@
449
479
 
450
480
  function set_cell_widths(): void {
451
481
  const column_count = data[0]?.length || 0;
482
+ if ($df_state.filter_state.filter_columns.length > 0) {
483
+ return;
484
+ }
452
485
  if (
453
486
  last_width_data_length === data.length &&
454
487
  last_width_column_count === column_count &&
@@ -513,6 +546,26 @@
513
546
  selected = result.selected;
514
547
  }
515
548
 
549
+ function filter_data(
550
+ _data: typeof data,
551
+ _display_value: string[][] | null,
552
+ _styling: string[][] | null
553
+ ): void {
554
+ const result = filter_data_and_preserve_selection(
555
+ _data,
556
+ _display_value,
557
+ _styling,
558
+ $df_state.filter_state.filter_columns,
559
+ selected,
560
+ get_current_indices,
561
+ $df_state.filter_state.initial_data?.data,
562
+ $df_state.filter_state.initial_data?.display_value,
563
+ $df_state.filter_state.initial_data?.styling
564
+ );
565
+ data = result.data;
566
+ selected = result.selected;
567
+ }
568
+
516
569
  $: selected_index = !!selected && selected[0];
517
570
 
518
571
  let is_visible = false;
@@ -802,10 +855,10 @@
802
855
  {toggle_header_menu}
803
856
  {end_header_edit}
804
857
  sort_columns={$df_state.sort_state.sort_columns}
858
+ filter_columns={$df_state.filter_state.filter_columns}
805
859
  {latex_delimiters}
806
860
  {line_breaks}
807
861
  {max_chars}
808
- {root}
809
862
  {editable}
810
863
  is_static={static_columns.includes(i)}
811
864
  {i18n}
@@ -830,7 +883,6 @@
830
883
  datatype={Array.isArray(datatype) ? datatype[j] : datatype}
831
884
  edit={false}
832
885
  el={null}
833
- {root}
834
886
  {editable}
835
887
  {i18n}
836
888
  show_selection_buttons={selected_cells.length === 1 &&
@@ -908,10 +960,10 @@
908
960
  {toggle_header_menu}
909
961
  {end_header_edit}
910
962
  sort_columns={$df_state.sort_state.sort_columns}
963
+ filter_columns={$df_state.filter_state.filter_columns}
911
964
  {latex_delimiters}
912
965
  {line_breaks}
913
966
  {max_chars}
914
- {root}
915
967
  {editable}
916
968
  is_static={static_columns.includes(i)}
917
969
  {i18n}
@@ -949,7 +1001,6 @@
949
1001
  datatype={Array.isArray(datatype) ? datatype[j] : datatype}
950
1002
  {editing}
951
1003
  {max_chars}
952
- {root}
953
1004
  {editable}
954
1005
  is_static={static_columns.includes(j)}
955
1006
  {i18n}
@@ -1026,6 +1077,25 @@
1026
1077
  (item) => item.col === (active_header_menu?.col ?? -1)
1027
1078
  ) + 1 || null
1028
1079
  : null}
1080
+ on_filter={active_header_menu
1081
+ ? (datatype, filter, value) => {
1082
+ if (active_header_menu) {
1083
+ handle_filter(active_header_menu.col, datatype, filter, value);
1084
+ df_actions.set_active_header_menu(null);
1085
+ }
1086
+ }
1087
+ : undefined}
1088
+ on_clear_filter={active_header_menu
1089
+ ? () => {
1090
+ clear_filter();
1091
+ df_actions.set_active_header_menu(null);
1092
+ }
1093
+ : undefined}
1094
+ filter_active={active_header_menu
1095
+ ? $df_state.filter_state.filter_columns.some(
1096
+ (c) => c.col === (active_header_menu?.col ?? -1)
1097
+ )
1098
+ : null}
1029
1099
  />
1030
1100
  {/if}
1031
1101
 
@@ -53,7 +53,6 @@
53
53
  export let datatype: Datatype;
54
54
  export let editing: [number, number] | false;
55
55
  export let max_chars: number | undefined;
56
- export let root: string;
57
56
  export let editable: boolean;
58
57
  export let is_static = false;
59
58
  export let i18n: I18nFormatter;
@@ -139,7 +138,6 @@
139
138
  }
140
139
  }}
141
140
  on:blur={handle_blur}
142
- {root}
143
141
  {max_chars}
144
142
  {i18n}
145
143
  {components}
@@ -7,6 +7,8 @@
7
7
  import SortArrowUp from "./icons/SortArrowUp.svelte";
8
8
  import SortArrowDown from "./icons/SortArrowDown.svelte";
9
9
  import type { SortDirection } from "./context/dataframe_context";
10
+ import CellMenuIcons from "./CellMenuIcons.svelte";
11
+ import type { FilterDatatype } from "./context/dataframe_context";
10
12
  export let value: string;
11
13
  export let i: number;
12
14
  export let actual_pinned_columns: number;
@@ -18,6 +20,12 @@
18
20
  export let toggle_header_menu: (event: MouseEvent, col: number) => void;
19
21
  export let end_header_edit: (event: CustomEvent<KeyboardEvent>) => void;
20
22
  export let sort_columns: { col: number; direction: SortDirection }[] = [];
23
+ export let filter_columns: {
24
+ col: number;
25
+ datatype: FilterDatatype;
26
+ filter: string;
27
+ value: string;
28
+ }[] = [];
21
29
 
22
30
  export let latex_delimiters: {
23
31
  left: string;
@@ -26,7 +34,6 @@
26
34
  }[];
27
35
  export let line_breaks: boolean;
28
36
  export let max_chars: number | undefined;
29
- export let root: string;
30
37
  export let editable: boolean;
31
38
  export let i18n: I18nFormatter;
32
39
  export let el: HTMLInputElement | null;
@@ -35,6 +42,7 @@
35
42
 
36
43
  $: can_add_columns = col_count && col_count[1] === "dynamic";
37
44
  $: sort_index = sort_columns.findIndex((item) => item.col === i);
45
+ $: filter_index = filter_columns.findIndex((item) => item.col === i);
38
46
  $: sort_priority = sort_index !== -1 ? sort_index + 1 : null;
39
47
  $: current_direction =
40
48
  sort_index !== -1 ? sort_columns[sort_index].direction : null;
@@ -64,6 +72,7 @@
64
72
  class:last-pinned={i === actual_pinned_columns - 1}
65
73
  class:focus={header_edit === i || selected_header === i}
66
74
  class:sorted={sort_index !== -1}
75
+ class:filtered={filter_index !== -1}
67
76
  aria-sort={get_sort_status(value, sort_columns, headers) === "none"
68
77
  ? "none"
69
78
  : get_sort_status(value, sort_columns, headers) === "asc"
@@ -105,7 +114,6 @@
105
114
  }
106
115
  }}
107
116
  header
108
- {root}
109
117
  {editable}
110
118
  {is_static}
111
119
  {i18n}
@@ -127,6 +135,13 @@
127
135
  {/if}
128
136
  </div>
129
137
  {/if}
138
+ {#if filter_index !== -1}
139
+ <div class="filter-indicators">
140
+ <span class="filter-icon">
141
+ <CellMenuIcons icon="filter" />
142
+ </span>
143
+ </div>
144
+ {/if}
130
145
  </button>
131
146
  {#if is_static}
132
147
  <Padlock />
@@ -245,6 +260,20 @@
245
260
  padding: var(--size-1-5);
246
261
  }
247
262
 
263
+ .filter-indicators {
264
+ display: flex;
265
+ align-items: center;
266
+ margin-left: var(--size-1);
267
+ gap: var(--size-1);
268
+ }
269
+
270
+ .filter-icon {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ color: var(--body-text-color);
275
+ }
276
+
248
277
  .pinned-column {
249
278
  position: sticky;
250
279
  z-index: 5;