@gradio/dataframe 0.16.0 → 0.16.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @gradio/dataframe
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Fixes
6
+
7
+ - [#10607](https://github.com/gradio-app/gradio/pull/10607) [`c354f5f`](https://github.com/gradio-app/gradio/commit/c354f5ff16c787d722f4e53d5a97f729abba955e) - Add empty dataframe functionality. Thanks @hannahblair!
8
+ - [#10596](https://github.com/gradio-app/gradio/pull/10596) [`a8bde76`](https://github.com/gradio-app/gradio/commit/a8bde76e2b0f65b3565019beb03ac8b1fd152963) - Fix margin above `gr.Dataframe` when no header is provided. Thanks @abidlabs!
9
+
3
10
  ## 0.16.0
4
11
 
5
12
  ### Features
@@ -152,6 +152,21 @@
152
152
  }}
153
153
  />
154
154
 
155
+ <Story
156
+ name="Dataframe without a label"
157
+ args={{
158
+ values: [
159
+ [800, 100, 800],
160
+ [200, 800, 700]
161
+ ],
162
+ headers: ["Math", "Reading", "Writing"],
163
+ show_label: false,
164
+ col_count: [3, "dynamic"],
165
+ row_count: [2, "dynamic"],
166
+ editable: false
167
+ }}
168
+ />
169
+
155
170
  <Story
156
171
  name="Dataframe with different colors"
157
172
  args={{
package/Example.svelte CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  let hovered = false;
8
8
  let loaded = Array.isArray(value);
9
+ let is_empty = loaded && (value.length === 0 || value[0].length === 0);
9
10
  </script>
10
11
 
11
12
  {#if loaded}
@@ -20,6 +21,12 @@
20
21
  >
21
22
  {#if typeof value === "string"}
22
23
  {value}
24
+ {:else if is_empty}
25
+ <table class="">
26
+ <tr>
27
+ <td>Empty</td>
28
+ </tr>
29
+ </table>
23
30
  {:else}
24
31
  <table class="">
25
32
  {#each value.slice(0, 3) as row, i}
@@ -4,6 +4,7 @@ export let selected = false;
4
4
  export let index;
5
5
  let hovered = false;
6
6
  let loaded = Array.isArray(value);
7
+ let is_empty = loaded && (value.length === 0 || value[0].length === 0);
7
8
  </script>
8
9
 
9
10
  {#if loaded}
@@ -18,6 +19,12 @@ let loaded = Array.isArray(value);
18
19
  >
19
20
  {#if typeof value === "string"}
20
21
  {value}
22
+ {:else if is_empty}
23
+ <table class="">
24
+ <tr>
25
+ <td>Empty</td>
26
+ </tr>
27
+ </table>
21
28
  {:else}
22
29
  <table class="">
23
30
  {#each value.slice(0, 3) as row, i}
@@ -81,7 +81,7 @@ function position_menu() {
81
81
  <style>
82
82
  .cell-menu {
83
83
  position: fixed;
84
- z-index: var(--layer-2);
84
+ z-index: var(--layer-4);
85
85
  background: var(--background-fill-primary);
86
86
  border: 1px solid var(--border-color-primary);
87
87
  border-radius: var(--radius-sm);
@@ -84,7 +84,6 @@ function handle_click() {
84
84
  class:multiline={header}
85
85
  on:focus|preventDefault
86
86
  style={styling}
87
- class="table-cell-text"
88
87
  data-editable={editable}
89
88
  placeholder=" "
90
89
  >
@@ -123,8 +122,6 @@ function handle_click() {
123
122
  position: relative;
124
123
  display: inline-block;
125
124
  outline: none;
126
- padding: var(--size-2);
127
- padding-right: 0;
128
125
  -webkit-user-select: text;
129
126
  -moz-user-select: text;
130
127
  -ms-user-select: text;
@@ -151,6 +148,7 @@ function handle_click() {
151
148
  font-weight: var(--weight-bold);
152
149
  white-space: normal;
153
150
  word-break: break-word;
151
+ margin-left: var(--size-1);
154
152
  }
155
153
 
156
154
  .edit {
@@ -109,9 +109,11 @@ function make_headers(_head, col_count2, els2) {
109
109
  }
110
110
  function process_data(_values) {
111
111
  const data_row_length = _values.length;
112
+ if (data_row_length === 0)
113
+ return [];
112
114
  return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length).fill(0).map((_, i) => {
113
115
  return Array(
114
- col_count[1] === "fixed" ? col_count[0] : data_row_length > 0 ? _values[0].length : headers.length
116
+ col_count[1] === "fixed" ? col_count[0] : _values[0].length || headers.length
115
117
  ).fill(0).map((_2, j) => {
116
118
  const id = make_id();
117
119
  els[id] = els[id] || { input: null, cell: null };
@@ -360,16 +362,14 @@ async function add_row(index) {
360
362
  parent.focus();
361
363
  if (row_count[1] !== "dynamic")
362
364
  return;
363
- if (data.length === 0) {
364
- values = [Array(headers.length).fill("")];
365
- return;
366
- }
367
- const new_row = Array(data[0].length).fill(0).map((_, i) => {
365
+ const new_row = Array(data[0]?.length || headers.length).fill(0).map((_, i) => {
368
366
  const _id = make_id();
369
367
  els[_id] = { cell: null, input: null };
370
368
  return { id: _id, value: "" };
371
369
  });
372
- if (index !== void 0 && index >= 0 && index <= data.length) {
370
+ if (data.length === 0) {
371
+ data = [new_row];
372
+ } else if (index !== void 0 && index >= 0 && index <= data.length) {
373
373
  data.splice(index, 0, new_row);
374
374
  } else {
375
375
  data.push(new_row);
@@ -605,14 +605,16 @@ async function delete_col(index) {
605
605
  parent.focus();
606
606
  if (col_count[1] !== "dynamic")
607
607
  return;
608
- if (data[0].length <= 1)
608
+ if (_headers.length <= 1)
609
609
  return;
610
610
  _headers.splice(index, 1);
611
611
  _headers = _headers;
612
- data.forEach((row) => {
613
- row.splice(index, 1);
614
- });
615
- data = data;
612
+ if (data.length > 0) {
613
+ data.forEach((row) => {
614
+ row.splice(index, 1);
615
+ });
616
+ data = data;
617
+ }
616
618
  selected = false;
617
619
  }
618
620
  function delete_row_at(index) {
@@ -702,24 +704,26 @@ function commit_filter() {
702
704
  <svelte:window on:resize={() => set_cell_widths()} />
703
705
 
704
706
  <div class="table-container">
705
- <div class="header-row">
706
- {#if label && label.length !== 0 && show_label}
707
- <div class="label">
708
- <p>{label}</p>
709
- </div>
710
- {/if}
711
- <Toolbar
712
- {show_fullscreen_button}
713
- {is_fullscreen}
714
- on:click={toggle_fullscreen}
715
- on_copy={handle_copy}
716
- {show_copy_button}
717
- {show_search}
718
- on:search={(e) => handle_search(e.detail)}
719
- on_commit_filter={commit_filter}
720
- {current_search_query}
721
- />
722
- </div>
707
+ {#if (label && label.length !== 0 && show_label) || show_fullscreen_button || show_copy_button || show_search !== "none"}
708
+ <div class="header-row">
709
+ {#if label && label.length !== 0 && show_label}
710
+ <div class="label">
711
+ <p>{label}</p>
712
+ </div>
713
+ {/if}
714
+ <Toolbar
715
+ {show_fullscreen_button}
716
+ {is_fullscreen}
717
+ on:click={toggle_fullscreen}
718
+ on_copy={handle_copy}
719
+ {show_copy_button}
720
+ {show_search}
721
+ on:search={(e) => handle_search(e.detail)}
722
+ on_commit_filter={commit_filter}
723
+ {current_search_query}
724
+ />
725
+ </div>
726
+ {/if}
723
727
  <div
724
728
  bind:this={parent}
725
729
  class="table-wrap"
@@ -864,184 +868,205 @@ function commit_filter() {
864
868
  bind:dragging
865
869
  aria_label={i18n("dataframe.drop_to_upload")}
866
870
  >
867
- <VirtualTable
868
- bind:items={data}
869
- {max_height}
870
- bind:actual_height={table_height}
871
- bind:table_scrollbar_width={scrollbar_width}
872
- selected={selected_index}
873
- disable_scroll={active_cell_menu !== null ||
874
- active_header_menu !== null}
875
- >
876
- {#if label && label.length !== 0}
877
- <caption class="sr-only">{label}</caption>
878
- {/if}
879
- <tr slot="thead">
880
- {#if show_row_numbers}
881
- <th
882
- class="row-number-header frozen-column always-frozen"
883
- style="left: 0;"
884
- >
885
- <div class="cell-wrap">
886
- <div class="header-content">
887
- <div class="header-text"></div>
888
- </div>
889
- </div>
890
- </th>
871
+ <div class="table-wrap">
872
+ <VirtualTable
873
+ bind:items={data}
874
+ {max_height}
875
+ bind:actual_height={table_height}
876
+ bind:table_scrollbar_width={scrollbar_width}
877
+ selected={selected_index}
878
+ disable_scroll={active_cell_menu !== null ||
879
+ active_header_menu !== null}
880
+ >
881
+ {#if label && label.length !== 0}
882
+ <caption class="sr-only">{label}</caption>
891
883
  {/if}
892
- {#each _headers as { value, id }, i (id)}
893
- <th
894
- class:frozen-column={i < actual_pinned_columns}
895
- class:last-frozen={i === actual_pinned_columns - 1}
896
- class:focus={header_edit === i || selected_header === i}
897
- aria-sort={get_sort_status(value, sort_by, sort_direction)}
898
- style="width: {get_cell_width(i)}; left: {i <
899
- actual_pinned_columns
900
- ? i === 0
901
- ? show_row_numbers
902
- ? 'var(--cell-width-row-number)'
903
- : '0'
904
- : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
905
- i
906
- )
907
- .fill(0)
908
- .map((_, idx) => `var(--cell-width-${idx})`)
909
- .join(' + ')})`
910
- : 'auto'};"
911
- on:click={() => {
912
- toggle_header_button(i);
913
- }}
914
- >
915
- <div class="cell-wrap">
916
- <div class="header-content">
884
+ <tr slot="thead">
885
+ {#if show_row_numbers}
886
+ <th
887
+ class="row-number-header frozen-column always-frozen"
888
+ style="left: 0;"
889
+ >
890
+ <div class="cell-wrap">
891
+ <div class="header-content">
892
+ <div class="header-text"></div>
893
+ </div>
894
+ </div>
895
+ </th>
896
+ {/if}
897
+ {#each _headers as { value, id }, i (id)}
898
+ <th
899
+ class:frozen-column={i < actual_pinned_columns}
900
+ class:last-frozen={i === actual_pinned_columns - 1}
901
+ class:focus={header_edit === i || selected_header === i}
902
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
903
+ style="width: {get_cell_width(i)}; left: {i <
904
+ actual_pinned_columns
905
+ ? i === 0
906
+ ? show_row_numbers
907
+ ? 'var(--cell-width-row-number)'
908
+ : '0'
909
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
910
+ i
911
+ )
912
+ .fill(0)
913
+ .map((_, idx) => `var(--cell-width-${idx})`)
914
+ .join(' + ')})`
915
+ : 'auto'};"
916
+ on:click={() => {
917
+ toggle_header_button(i);
918
+ }}
919
+ >
920
+ <div class="cell-wrap">
921
+ <div class="header-content">
922
+ <EditableCell
923
+ {max_chars}
924
+ bind:value={_headers[i].value}
925
+ bind:el={els[id].input}
926
+ {latex_delimiters}
927
+ {line_breaks}
928
+ edit={header_edit === i}
929
+ on:keydown={end_header_edit}
930
+ on:dblclick={() => edit_header(i)}
931
+ header
932
+ {root}
933
+ {editable}
934
+ />
935
+ <div class="sort-buttons">
936
+ <SortIcon
937
+ direction={sort_by === i ? sort_direction : null}
938
+ on:sort={({ detail }) => handle_sort(i, detail)}
939
+ {i18n}
940
+ />
941
+ </div>
942
+ </div>
943
+ {#if editable}
944
+ <button
945
+ class="cell-menu-button"
946
+ on:click={(event) => toggle_header_menu(event, i)}
947
+ on:touchstart={(event) => {
948
+ event.preventDefault();
949
+ const touch = event.touches[0];
950
+ const mouseEvent = new MouseEvent("click", {
951
+ clientX: touch.clientX,
952
+ clientY: touch.clientY,
953
+ bubbles: true,
954
+ cancelable: true,
955
+ view: window
956
+ });
957
+ toggle_header_menu(mouseEvent, i);
958
+ }}
959
+ >
960
+ &#8942;
961
+ </button>
962
+ {/if}
963
+ </div>
964
+ </th>
965
+ {/each}
966
+ </tr>
967
+ <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
968
+ {#if show_row_numbers}
969
+ <td
970
+ class="row-number frozen-column always-frozen"
971
+ style="left: 0;"
972
+ tabindex="-1"
973
+ >
974
+ {index + 1}
975
+ </td>
976
+ {/if}
977
+ {#each item as { value, id }, j (id)}
978
+ <td
979
+ class:frozen-column={j < actual_pinned_columns}
980
+ class:last-frozen={j === actual_pinned_columns - 1}
981
+ tabindex={show_row_numbers && j === 0 ? -1 : 0}
982
+ bind:this={els[id].cell}
983
+ on:touchstart={(event) => {
984
+ const touch = event.touches[0];
985
+ const mouseEvent = new MouseEvent("click", {
986
+ clientX: touch.clientX,
987
+ clientY: touch.clientY,
988
+ bubbles: true,
989
+ cancelable: true,
990
+ view: window
991
+ });
992
+ handle_cell_click(mouseEvent, index, j);
993
+ }}
994
+ on:mousedown={(event) => {
995
+ event.preventDefault();
996
+ event.stopPropagation();
997
+ }}
998
+ on:click={(event) => handle_cell_click(event, index, j)}
999
+ style="width: {get_cell_width(j)}; left: {j <
1000
+ actual_pinned_columns
1001
+ ? j === 0
1002
+ ? show_row_numbers
1003
+ ? 'var(--cell-width-row-number)'
1004
+ : '0'
1005
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1006
+ j
1007
+ )
1008
+ .fill(0)
1009
+ .map((_, idx) => `var(--cell-width-${idx})`)
1010
+ .join(' + ')})`
1011
+ : 'auto'}; {styling?.[index]?.[j] || ''}"
1012
+ class:flash={copy_flash &&
1013
+ is_cell_selected([index, j], selected_cells)}
1014
+ class={is_cell_selected([index, j], selected_cells)}
1015
+ class:menu-active={active_cell_menu &&
1016
+ active_cell_menu.row === index &&
1017
+ active_cell_menu.col === j}
1018
+ >
1019
+ <div class="cell-wrap">
917
1020
  <EditableCell
918
- {max_chars}
919
- bind:value={_headers[i].value}
1021
+ bind:value={data[index][j].value}
920
1022
  bind:el={els[id].input}
1023
+ display_value={display_value?.[index]?.[j]}
921
1024
  {latex_delimiters}
922
1025
  {line_breaks}
923
- edit={header_edit === i}
924
- on:keydown={end_header_edit}
925
- on:dblclick={() => edit_header(i)}
926
- header
927
- {root}
928
1026
  {editable}
1027
+ edit={dequal(editing, [index, j])}
1028
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1029
+ on:blur={() => {
1030
+ clear_on_focus = false;
1031
+ parent.focus();
1032
+ }}
1033
+ on:focus={() => {
1034
+ const row = index;
1035
+ const col = j;
1036
+ if (
1037
+ !selected_cells.some(([r, c]) => r === row && c === col)
1038
+ ) {
1039
+ selected_cells = [[row, col]];
1040
+ }
1041
+ }}
1042
+ {clear_on_focus}
1043
+ {root}
1044
+ {max_chars}
929
1045
  />
930
- <div class="sort-buttons">
931
- <SortIcon
932
- direction={sort_by === i ? sort_direction : null}
933
- on:sort={({ detail }) => handle_sort(i, detail)}
934
- {i18n}
935
- />
936
- </div>
1046
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1047
+ <button
1048
+ class="cell-menu-button"
1049
+ on:click={(event) => toggle_cell_menu(event, index, j)}
1050
+ >
1051
+ &#8942;
1052
+ </button>
1053
+ {/if}
937
1054
  </div>
938
- {#if editable}
939
- <button
940
- class="cell-menu-button"
941
- on:click={(event) => toggle_header_menu(event, i)}
942
- >
943
- &#8942;
944
- </button>
945
- {/if}
946
- </div>
947
- </th>
948
- {/each}
949
- </tr>
950
- <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
951
- {#if show_row_numbers}
952
- <td
953
- class="row-number frozen-column always-frozen"
954
- style="left: 0;"
955
- tabindex="-1"
956
- >
957
- {index + 1}
958
- </td>
959
- {/if}
960
- {#each item as { value, id }, j (id)}
961
- <td
962
- class:frozen-column={j < actual_pinned_columns}
963
- class:last-frozen={j === actual_pinned_columns - 1}
964
- tabindex={show_row_numbers && j === 0 ? -1 : 0}
965
- bind:this={els[id].cell}
966
- on:touchstart={(event) => {
967
- const touch = event.touches[0];
968
- const mouseEvent = new MouseEvent("click", {
969
- clientX: touch.clientX,
970
- clientY: touch.clientY,
971
- bubbles: true,
972
- cancelable: true,
973
- view: window
974
- });
975
- handle_cell_click(mouseEvent, index, j);
976
- }}
977
- on:mousedown={(event) => {
978
- event.preventDefault();
979
- event.stopPropagation();
980
- }}
981
- on:click={(event) => handle_cell_click(event, index, j)}
982
- style="width: {get_cell_width(j)}; left: {j <
983
- actual_pinned_columns
984
- ? j === 0
985
- ? show_row_numbers
986
- ? 'var(--cell-width-row-number)'
987
- : '0'
988
- : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
989
- j
990
- )
991
- .fill(0)
992
- .map((_, idx) => `var(--cell-width-${idx})`)
993
- .join(' + ')})`
994
- : 'auto'}; {styling?.[index]?.[j] || ''}"
995
- class:flash={copy_flash &&
996
- is_cell_selected([index, j], selected_cells)}
997
- class={is_cell_selected([index, j], selected_cells)}
998
- class:menu-active={active_cell_menu &&
999
- active_cell_menu.row === index &&
1000
- active_cell_menu.col === j}
1001
- >
1002
- <div class="cell-wrap">
1003
- <EditableCell
1004
- bind:value={data[index][j].value}
1005
- bind:el={els[id].input}
1006
- display_value={display_value?.[index]?.[j]}
1007
- {latex_delimiters}
1008
- {line_breaks}
1009
- {editable}
1010
- edit={dequal(editing, [index, j])}
1011
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1012
- on:blur={() => {
1013
- clear_on_focus = false;
1014
- parent.focus();
1015
- }}
1016
- on:focus={() => {
1017
- const row = index;
1018
- const col = j;
1019
- if (
1020
- !selected_cells.some(([r, c]) => r === row && c === col)
1021
- ) {
1022
- selected_cells = [[row, col]];
1023
- }
1024
- }}
1025
- {clear_on_focus}
1026
- {root}
1027
- {max_chars}
1028
- />
1029
- {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1030
- <button
1031
- class="cell-menu-button"
1032
- on:click={(event) => toggle_cell_menu(event, index, j)}
1033
- >
1034
- &#8942;
1035
- </button>
1036
- {/if}
1037
- </div>
1038
- </td>
1039
- {/each}
1040
- </tr>
1041
- </VirtualTable>
1055
+ </td>
1056
+ {/each}
1057
+ </tr>
1058
+ </VirtualTable>
1059
+ </div>
1042
1060
  </Upload>
1043
1061
  </div>
1044
1062
  </div>
1063
+ {#if data.length === 0 && editable && row_count[1] === "dynamic"}
1064
+ <div class="add-row-container">
1065
+ <button class="add-row-button" on:click={() => add_row()}>
1066
+ <span>+</span>
1067
+ </button>
1068
+ </div>
1069
+ {/if}
1045
1070
 
1046
1071
  {#if active_cell_menu}
1047
1072
  <CellMenu
@@ -1078,7 +1103,7 @@ function commit_filter() {
1078
1103
  on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
1079
1104
  on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
1080
1105
  can_delete_rows={false}
1081
- can_delete_cols={data[0].length > 1}
1106
+ can_delete_cols={_headers.length > 1}
1082
1107
  />
1083
1108
  {/if}
1084
1109
 
@@ -1100,8 +1125,6 @@ function commit_filter() {
1100
1125
  .table-wrap {
1101
1126
  position: relative;
1102
1127
  transition: 150ms;
1103
- border: 1px solid var(--border-color-primary);
1104
- border-radius: var(--table-radius);
1105
1128
  }
1106
1129
 
1107
1130
  .table-wrap.menu-open {
@@ -1134,6 +1157,12 @@ function commit_filter() {
1134
1157
  border-collapse: separate;
1135
1158
  }
1136
1159
 
1160
+ .table-wrap > :global(button) {
1161
+ border: 1px solid var(--border-color-primary);
1162
+ border-radius: var(--table-radius);
1163
+ overflow: hidden;
1164
+ }
1165
+
1137
1166
  div:not(.no-wrap) td {
1138
1167
  overflow-wrap: anywhere;
1139
1168
  }
@@ -1176,10 +1205,12 @@ function commit_filter() {
1176
1205
 
1177
1206
  th:first-child {
1178
1207
  border-top-left-radius: var(--table-radius);
1208
+ border-bottom-left-radius: var(--table-radius);
1179
1209
  }
1180
1210
 
1181
1211
  th:last-child {
1182
1212
  border-top-right-radius: var(--table-radius);
1213
+ border-bottom-right-radius: var(--table-radius);
1183
1214
  }
1184
1215
 
1185
1216
  th.focus,
@@ -1209,6 +1240,7 @@ function commit_filter() {
1209
1240
  display: flex;
1210
1241
  align-items: center;
1211
1242
  flex-shrink: 0;
1243
+ order: -1;
1212
1244
  }
1213
1245
 
1214
1246
  .editing {
@@ -1217,17 +1249,24 @@ function commit_filter() {
1217
1249
 
1218
1250
  .cell-wrap {
1219
1251
  display: flex;
1220
- align-items: flex-start;
1252
+ align-items: center;
1253
+ justify-content: flex-start;
1221
1254
  outline: none;
1222
1255
  min-height: var(--size-9);
1223
1256
  position: relative;
1224
- height: auto;
1257
+ height: 100%;
1258
+ padding: var(--size-2);
1259
+ box-sizing: border-box;
1260
+ margin: 0;
1261
+ gap: var(--size-1);
1262
+ overflow: visible;
1263
+ min-width: 0;
1264
+ border-radius: var(--table-radius);
1225
1265
  }
1226
1266
 
1227
1267
  .header-content {
1228
1268
  display: flex;
1229
1269
  align-items: center;
1230
- justify-content: space-between;
1231
1270
  overflow: hidden;
1232
1271
  flex-grow: 1;
1233
1272
  min-width: 0;
@@ -1235,7 +1274,6 @@ function commit_filter() {
1235
1274
  overflow-wrap: break-word;
1236
1275
  word-break: normal;
1237
1276
  height: 100%;
1238
- padding: var(--size-1);
1239
1277
  gap: var(--size-1);
1240
1278
  }
1241
1279
 
@@ -1265,7 +1303,8 @@ function commit_filter() {
1265
1303
  transform: translateY(-50%);
1266
1304
  }
1267
1305
 
1268
- .cell-selected .cell-menu-button {
1306
+ .cell-selected .cell-menu-button,
1307
+ th:hover .cell-menu-button {
1269
1308
  display: flex;
1270
1309
  align-items: center;
1271
1310
  justify-content: center;
@@ -1482,4 +1521,24 @@ function commit_filter() {
1482
1521
  .always-frozen {
1483
1522
  z-index: var(--layer-3);
1484
1523
  }
1524
+
1525
+ .add-row-container {
1526
+ margin-top: var(--size-2);
1527
+ }
1528
+
1529
+ .add-row-button {
1530
+ width: 100%;
1531
+ padding: var(--size-1);
1532
+ background: transparent;
1533
+ border: 1px dashed var(--border-color-primary);
1534
+ border-radius: var(--radius-sm);
1535
+ color: var(--body-text-color);
1536
+ cursor: pointer;
1537
+ transition: all 150ms;
1538
+ }
1539
+
1540
+ .add-row-button:hover {
1541
+ background: var(--background-fill-secondary);
1542
+ border-style: solid;
1543
+ }
1485
1544
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/dataframe",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -18,12 +18,12 @@
18
18
  "katex": "^0.16.7",
19
19
  "marked": "^12.0.0",
20
20
  "@gradio/atoms": "^0.13.2",
21
- "@gradio/button": "^0.4.6",
22
- "@gradio/markdown-code": "^0.4.0",
23
21
  "@gradio/client": "^1.12.0",
24
- "@gradio/statustracker": "^0.10.3",
25
22
  "@gradio/icons": "^0.10.0",
23
+ "@gradio/button": "^0.4.6",
24
+ "@gradio/markdown-code": "^0.4.0",
26
25
  "@gradio/upload": "^0.15.1",
26
+ "@gradio/statustracker": "^0.10.3",
27
27
  "@gradio/utils": "^0.10.1"
28
28
  },
29
29
  "exports": {
@@ -89,7 +89,7 @@
89
89
  <style>
90
90
  .cell-menu {
91
91
  position: fixed;
92
- z-index: var(--layer-2);
92
+ z-index: var(--layer-4);
93
93
  background: var(--background-fill-primary);
94
94
  border: 1px solid var(--border-color-primary);
95
95
  border-radius: var(--radius-sm);
@@ -110,7 +110,6 @@
110
110
  class:multiline={header}
111
111
  on:focus|preventDefault
112
112
  style={styling}
113
- class="table-cell-text"
114
113
  data-editable={editable}
115
114
  placeholder=" "
116
115
  >
@@ -149,8 +148,6 @@
149
148
  position: relative;
150
149
  display: inline-block;
151
150
  outline: none;
152
- padding: var(--size-2);
153
- padding-right: 0;
154
151
  -webkit-user-select: text;
155
152
  -moz-user-select: text;
156
153
  -ms-user-select: text;
@@ -177,6 +174,7 @@
177
174
  font-weight: var(--weight-bold);
178
175
  white-space: normal;
179
176
  word-break: break-word;
177
+ margin-left: var(--size-1);
180
178
  }
181
179
 
182
180
  .edit {
@@ -176,15 +176,14 @@
176
176
  id: string;
177
177
  }[][] {
178
178
  const data_row_length = _values.length;
179
+ if (data_row_length === 0) return [];
179
180
  return Array(row_count[1] === "fixed" ? row_count[0] : data_row_length)
180
181
  .fill(0)
181
182
  .map((_, i) => {
182
183
  return Array(
183
184
  col_count[1] === "fixed"
184
185
  ? col_count[0]
185
- : data_row_length > 0
186
- ? _values[0].length
187
- : headers.length
186
+ : _values[0].length || headers.length
188
187
  )
189
188
  .fill(0)
190
189
  .map((_, j) => {
@@ -473,12 +472,8 @@
473
472
  parent.focus();
474
473
 
475
474
  if (row_count[1] !== "dynamic") return;
476
- if (data.length === 0) {
477
- values = [Array(headers.length).fill("")];
478
- return;
479
- }
480
475
 
481
- const new_row = Array(data[0].length)
476
+ const new_row = Array(data[0]?.length || headers.length)
482
477
  .fill(0)
483
478
  .map((_, i) => {
484
479
  const _id = make_id();
@@ -486,7 +481,9 @@
486
481
  return { id: _id, value: "" };
487
482
  });
488
483
 
489
- if (index !== undefined && index >= 0 && index <= data.length) {
484
+ if (data.length === 0) {
485
+ data = [new_row];
486
+ } else if (index !== undefined && index >= 0 && index <= data.length) {
490
487
  data.splice(index, 0, new_row);
491
488
  } else {
492
489
  data.push(new_row);
@@ -786,15 +783,17 @@
786
783
  async function delete_col(index: number): Promise<void> {
787
784
  parent.focus();
788
785
  if (col_count[1] !== "dynamic") return;
789
- if (data[0].length <= 1) return;
786
+ if (_headers.length <= 1) return;
790
787
 
791
788
  _headers.splice(index, 1);
792
789
  _headers = _headers;
793
790
 
794
- data.forEach((row) => {
795
- row.splice(index, 1);
796
- });
797
- data = data;
791
+ if (data.length > 0) {
792
+ data.forEach((row) => {
793
+ row.splice(index, 1);
794
+ });
795
+ data = data;
796
+ }
798
797
  selected = false;
799
798
  }
800
799
 
@@ -902,24 +901,26 @@
902
901
  <svelte:window on:resize={() => set_cell_widths()} />
903
902
 
904
903
  <div class="table-container">
905
- <div class="header-row">
906
- {#if label && label.length !== 0 && show_label}
907
- <div class="label">
908
- <p>{label}</p>
909
- </div>
910
- {/if}
911
- <Toolbar
912
- {show_fullscreen_button}
913
- {is_fullscreen}
914
- on:click={toggle_fullscreen}
915
- on_copy={handle_copy}
916
- {show_copy_button}
917
- {show_search}
918
- on:search={(e) => handle_search(e.detail)}
919
- on_commit_filter={commit_filter}
920
- {current_search_query}
921
- />
922
- </div>
904
+ {#if (label && label.length !== 0 && show_label) || show_fullscreen_button || show_copy_button || show_search !== "none"}
905
+ <div class="header-row">
906
+ {#if label && label.length !== 0 && show_label}
907
+ <div class="label">
908
+ <p>{label}</p>
909
+ </div>
910
+ {/if}
911
+ <Toolbar
912
+ {show_fullscreen_button}
913
+ {is_fullscreen}
914
+ on:click={toggle_fullscreen}
915
+ on_copy={handle_copy}
916
+ {show_copy_button}
917
+ {show_search}
918
+ on:search={(e) => handle_search(e.detail)}
919
+ on_commit_filter={commit_filter}
920
+ {current_search_query}
921
+ />
922
+ </div>
923
+ {/if}
923
924
  <div
924
925
  bind:this={parent}
925
926
  class="table-wrap"
@@ -1064,184 +1065,205 @@
1064
1065
  bind:dragging
1065
1066
  aria_label={i18n("dataframe.drop_to_upload")}
1066
1067
  >
1067
- <VirtualTable
1068
- bind:items={data}
1069
- {max_height}
1070
- bind:actual_height={table_height}
1071
- bind:table_scrollbar_width={scrollbar_width}
1072
- selected={selected_index}
1073
- disable_scroll={active_cell_menu !== null ||
1074
- active_header_menu !== null}
1075
- >
1076
- {#if label && label.length !== 0}
1077
- <caption class="sr-only">{label}</caption>
1078
- {/if}
1079
- <tr slot="thead">
1080
- {#if show_row_numbers}
1081
- <th
1082
- class="row-number-header frozen-column always-frozen"
1083
- style="left: 0;"
1084
- >
1085
- <div class="cell-wrap">
1086
- <div class="header-content">
1087
- <div class="header-text"></div>
1088
- </div>
1089
- </div>
1090
- </th>
1068
+ <div class="table-wrap">
1069
+ <VirtualTable
1070
+ bind:items={data}
1071
+ {max_height}
1072
+ bind:actual_height={table_height}
1073
+ bind:table_scrollbar_width={scrollbar_width}
1074
+ selected={selected_index}
1075
+ disable_scroll={active_cell_menu !== null ||
1076
+ active_header_menu !== null}
1077
+ >
1078
+ {#if label && label.length !== 0}
1079
+ <caption class="sr-only">{label}</caption>
1091
1080
  {/if}
1092
- {#each _headers as { value, id }, i (id)}
1093
- <th
1094
- class:frozen-column={i < actual_pinned_columns}
1095
- class:last-frozen={i === actual_pinned_columns - 1}
1096
- class:focus={header_edit === i || selected_header === i}
1097
- aria-sort={get_sort_status(value, sort_by, sort_direction)}
1098
- style="width: {get_cell_width(i)}; left: {i <
1099
- actual_pinned_columns
1100
- ? i === 0
1101
- ? show_row_numbers
1102
- ? 'var(--cell-width-row-number)'
1103
- : '0'
1104
- : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1105
- i
1106
- )
1107
- .fill(0)
1108
- .map((_, idx) => `var(--cell-width-${idx})`)
1109
- .join(' + ')})`
1110
- : 'auto'};"
1111
- on:click={() => {
1112
- toggle_header_button(i);
1113
- }}
1114
- >
1115
- <div class="cell-wrap">
1116
- <div class="header-content">
1081
+ <tr slot="thead">
1082
+ {#if show_row_numbers}
1083
+ <th
1084
+ class="row-number-header frozen-column always-frozen"
1085
+ style="left: 0;"
1086
+ >
1087
+ <div class="cell-wrap">
1088
+ <div class="header-content">
1089
+ <div class="header-text"></div>
1090
+ </div>
1091
+ </div>
1092
+ </th>
1093
+ {/if}
1094
+ {#each _headers as { value, id }, i (id)}
1095
+ <th
1096
+ class:frozen-column={i < actual_pinned_columns}
1097
+ class:last-frozen={i === actual_pinned_columns - 1}
1098
+ class:focus={header_edit === i || selected_header === i}
1099
+ aria-sort={get_sort_status(value, sort_by, sort_direction)}
1100
+ style="width: {get_cell_width(i)}; left: {i <
1101
+ actual_pinned_columns
1102
+ ? i === 0
1103
+ ? show_row_numbers
1104
+ ? 'var(--cell-width-row-number)'
1105
+ : '0'
1106
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1107
+ i
1108
+ )
1109
+ .fill(0)
1110
+ .map((_, idx) => `var(--cell-width-${idx})`)
1111
+ .join(' + ')})`
1112
+ : 'auto'};"
1113
+ on:click={() => {
1114
+ toggle_header_button(i);
1115
+ }}
1116
+ >
1117
+ <div class="cell-wrap">
1118
+ <div class="header-content">
1119
+ <EditableCell
1120
+ {max_chars}
1121
+ bind:value={_headers[i].value}
1122
+ bind:el={els[id].input}
1123
+ {latex_delimiters}
1124
+ {line_breaks}
1125
+ edit={header_edit === i}
1126
+ on:keydown={end_header_edit}
1127
+ on:dblclick={() => edit_header(i)}
1128
+ header
1129
+ {root}
1130
+ {editable}
1131
+ />
1132
+ <div class="sort-buttons">
1133
+ <SortIcon
1134
+ direction={sort_by === i ? sort_direction : null}
1135
+ on:sort={({ detail }) => handle_sort(i, detail)}
1136
+ {i18n}
1137
+ />
1138
+ </div>
1139
+ </div>
1140
+ {#if editable}
1141
+ <button
1142
+ class="cell-menu-button"
1143
+ on:click={(event) => toggle_header_menu(event, i)}
1144
+ on:touchstart={(event) => {
1145
+ event.preventDefault();
1146
+ const touch = event.touches[0];
1147
+ const mouseEvent = new MouseEvent("click", {
1148
+ clientX: touch.clientX,
1149
+ clientY: touch.clientY,
1150
+ bubbles: true,
1151
+ cancelable: true,
1152
+ view: window
1153
+ });
1154
+ toggle_header_menu(mouseEvent, i);
1155
+ }}
1156
+ >
1157
+ &#8942;
1158
+ </button>
1159
+ {/if}
1160
+ </div>
1161
+ </th>
1162
+ {/each}
1163
+ </tr>
1164
+ <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
1165
+ {#if show_row_numbers}
1166
+ <td
1167
+ class="row-number frozen-column always-frozen"
1168
+ style="left: 0;"
1169
+ tabindex="-1"
1170
+ >
1171
+ {index + 1}
1172
+ </td>
1173
+ {/if}
1174
+ {#each item as { value, id }, j (id)}
1175
+ <td
1176
+ class:frozen-column={j < actual_pinned_columns}
1177
+ class:last-frozen={j === actual_pinned_columns - 1}
1178
+ tabindex={show_row_numbers && j === 0 ? -1 : 0}
1179
+ bind:this={els[id].cell}
1180
+ on:touchstart={(event) => {
1181
+ const touch = event.touches[0];
1182
+ const mouseEvent = new MouseEvent("click", {
1183
+ clientX: touch.clientX,
1184
+ clientY: touch.clientY,
1185
+ bubbles: true,
1186
+ cancelable: true,
1187
+ view: window
1188
+ });
1189
+ handle_cell_click(mouseEvent, index, j);
1190
+ }}
1191
+ on:mousedown={(event) => {
1192
+ event.preventDefault();
1193
+ event.stopPropagation();
1194
+ }}
1195
+ on:click={(event) => handle_cell_click(event, index, j)}
1196
+ style="width: {get_cell_width(j)}; left: {j <
1197
+ actual_pinned_columns
1198
+ ? j === 0
1199
+ ? show_row_numbers
1200
+ ? 'var(--cell-width-row-number)'
1201
+ : '0'
1202
+ : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1203
+ j
1204
+ )
1205
+ .fill(0)
1206
+ .map((_, idx) => `var(--cell-width-${idx})`)
1207
+ .join(' + ')})`
1208
+ : 'auto'}; {styling?.[index]?.[j] || ''}"
1209
+ class:flash={copy_flash &&
1210
+ is_cell_selected([index, j], selected_cells)}
1211
+ class={is_cell_selected([index, j], selected_cells)}
1212
+ class:menu-active={active_cell_menu &&
1213
+ active_cell_menu.row === index &&
1214
+ active_cell_menu.col === j}
1215
+ >
1216
+ <div class="cell-wrap">
1117
1217
  <EditableCell
1118
- {max_chars}
1119
- bind:value={_headers[i].value}
1218
+ bind:value={data[index][j].value}
1120
1219
  bind:el={els[id].input}
1220
+ display_value={display_value?.[index]?.[j]}
1121
1221
  {latex_delimiters}
1122
1222
  {line_breaks}
1123
- edit={header_edit === i}
1124
- on:keydown={end_header_edit}
1125
- on:dblclick={() => edit_header(i)}
1126
- header
1127
- {root}
1128
1223
  {editable}
1224
+ edit={dequal(editing, [index, j])}
1225
+ datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1226
+ on:blur={() => {
1227
+ clear_on_focus = false;
1228
+ parent.focus();
1229
+ }}
1230
+ on:focus={() => {
1231
+ const row = index;
1232
+ const col = j;
1233
+ if (
1234
+ !selected_cells.some(([r, c]) => r === row && c === col)
1235
+ ) {
1236
+ selected_cells = [[row, col]];
1237
+ }
1238
+ }}
1239
+ {clear_on_focus}
1240
+ {root}
1241
+ {max_chars}
1129
1242
  />
1130
- <div class="sort-buttons">
1131
- <SortIcon
1132
- direction={sort_by === i ? sort_direction : null}
1133
- on:sort={({ detail }) => handle_sort(i, detail)}
1134
- {i18n}
1135
- />
1136
- </div>
1243
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1244
+ <button
1245
+ class="cell-menu-button"
1246
+ on:click={(event) => toggle_cell_menu(event, index, j)}
1247
+ >
1248
+ &#8942;
1249
+ </button>
1250
+ {/if}
1137
1251
  </div>
1138
- {#if editable}
1139
- <button
1140
- class="cell-menu-button"
1141
- on:click={(event) => toggle_header_menu(event, i)}
1142
- >
1143
- &#8942;
1144
- </button>
1145
- {/if}
1146
- </div>
1147
- </th>
1148
- {/each}
1149
- </tr>
1150
- <tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
1151
- {#if show_row_numbers}
1152
- <td
1153
- class="row-number frozen-column always-frozen"
1154
- style="left: 0;"
1155
- tabindex="-1"
1156
- >
1157
- {index + 1}
1158
- </td>
1159
- {/if}
1160
- {#each item as { value, id }, j (id)}
1161
- <td
1162
- class:frozen-column={j < actual_pinned_columns}
1163
- class:last-frozen={j === actual_pinned_columns - 1}
1164
- tabindex={show_row_numbers && j === 0 ? -1 : 0}
1165
- bind:this={els[id].cell}
1166
- on:touchstart={(event) => {
1167
- const touch = event.touches[0];
1168
- const mouseEvent = new MouseEvent("click", {
1169
- clientX: touch.clientX,
1170
- clientY: touch.clientY,
1171
- bubbles: true,
1172
- cancelable: true,
1173
- view: window
1174
- });
1175
- handle_cell_click(mouseEvent, index, j);
1176
- }}
1177
- on:mousedown={(event) => {
1178
- event.preventDefault();
1179
- event.stopPropagation();
1180
- }}
1181
- on:click={(event) => handle_cell_click(event, index, j)}
1182
- style="width: {get_cell_width(j)}; left: {j <
1183
- actual_pinned_columns
1184
- ? j === 0
1185
- ? show_row_numbers
1186
- ? 'var(--cell-width-row-number)'
1187
- : '0'
1188
- : `calc(${show_row_numbers ? 'var(--cell-width-row-number) + ' : ''}${Array(
1189
- j
1190
- )
1191
- .fill(0)
1192
- .map((_, idx) => `var(--cell-width-${idx})`)
1193
- .join(' + ')})`
1194
- : 'auto'}; {styling?.[index]?.[j] || ''}"
1195
- class:flash={copy_flash &&
1196
- is_cell_selected([index, j], selected_cells)}
1197
- class={is_cell_selected([index, j], selected_cells)}
1198
- class:menu-active={active_cell_menu &&
1199
- active_cell_menu.row === index &&
1200
- active_cell_menu.col === j}
1201
- >
1202
- <div class="cell-wrap">
1203
- <EditableCell
1204
- bind:value={data[index][j].value}
1205
- bind:el={els[id].input}
1206
- display_value={display_value?.[index]?.[j]}
1207
- {latex_delimiters}
1208
- {line_breaks}
1209
- {editable}
1210
- edit={dequal(editing, [index, j])}
1211
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1212
- on:blur={() => {
1213
- clear_on_focus = false;
1214
- parent.focus();
1215
- }}
1216
- on:focus={() => {
1217
- const row = index;
1218
- const col = j;
1219
- if (
1220
- !selected_cells.some(([r, c]) => r === row && c === col)
1221
- ) {
1222
- selected_cells = [[row, col]];
1223
- }
1224
- }}
1225
- {clear_on_focus}
1226
- {root}
1227
- {max_chars}
1228
- />
1229
- {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1230
- <button
1231
- class="cell-menu-button"
1232
- on:click={(event) => toggle_cell_menu(event, index, j)}
1233
- >
1234
- &#8942;
1235
- </button>
1236
- {/if}
1237
- </div>
1238
- </td>
1239
- {/each}
1240
- </tr>
1241
- </VirtualTable>
1252
+ </td>
1253
+ {/each}
1254
+ </tr>
1255
+ </VirtualTable>
1256
+ </div>
1242
1257
  </Upload>
1243
1258
  </div>
1244
1259
  </div>
1260
+ {#if data.length === 0 && editable && row_count[1] === "dynamic"}
1261
+ <div class="add-row-container">
1262
+ <button class="add-row-button" on:click={() => add_row()}>
1263
+ <span>+</span>
1264
+ </button>
1265
+ </div>
1266
+ {/if}
1245
1267
 
1246
1268
  {#if active_cell_menu}
1247
1269
  <CellMenu
@@ -1278,7 +1300,7 @@
1278
1300
  on_delete_row={() => delete_row_at(active_cell_menu?.row ?? -1)}
1279
1301
  on_delete_col={() => delete_col_at(active_header_menu?.col ?? -1)}
1280
1302
  can_delete_rows={false}
1281
- can_delete_cols={data[0].length > 1}
1303
+ can_delete_cols={_headers.length > 1}
1282
1304
  />
1283
1305
  {/if}
1284
1306
 
@@ -1300,8 +1322,6 @@
1300
1322
  .table-wrap {
1301
1323
  position: relative;
1302
1324
  transition: 150ms;
1303
- border: 1px solid var(--border-color-primary);
1304
- border-radius: var(--table-radius);
1305
1325
  }
1306
1326
 
1307
1327
  .table-wrap.menu-open {
@@ -1334,6 +1354,12 @@
1334
1354
  border-collapse: separate;
1335
1355
  }
1336
1356
 
1357
+ .table-wrap > :global(button) {
1358
+ border: 1px solid var(--border-color-primary);
1359
+ border-radius: var(--table-radius);
1360
+ overflow: hidden;
1361
+ }
1362
+
1337
1363
  div:not(.no-wrap) td {
1338
1364
  overflow-wrap: anywhere;
1339
1365
  }
@@ -1376,10 +1402,12 @@
1376
1402
 
1377
1403
  th:first-child {
1378
1404
  border-top-left-radius: var(--table-radius);
1405
+ border-bottom-left-radius: var(--table-radius);
1379
1406
  }
1380
1407
 
1381
1408
  th:last-child {
1382
1409
  border-top-right-radius: var(--table-radius);
1410
+ border-bottom-right-radius: var(--table-radius);
1383
1411
  }
1384
1412
 
1385
1413
  th.focus,
@@ -1409,6 +1437,7 @@
1409
1437
  display: flex;
1410
1438
  align-items: center;
1411
1439
  flex-shrink: 0;
1440
+ order: -1;
1412
1441
  }
1413
1442
 
1414
1443
  .editing {
@@ -1417,17 +1446,24 @@
1417
1446
 
1418
1447
  .cell-wrap {
1419
1448
  display: flex;
1420
- align-items: flex-start;
1449
+ align-items: center;
1450
+ justify-content: flex-start;
1421
1451
  outline: none;
1422
1452
  min-height: var(--size-9);
1423
1453
  position: relative;
1424
- height: auto;
1454
+ height: 100%;
1455
+ padding: var(--size-2);
1456
+ box-sizing: border-box;
1457
+ margin: 0;
1458
+ gap: var(--size-1);
1459
+ overflow: visible;
1460
+ min-width: 0;
1461
+ border-radius: var(--table-radius);
1425
1462
  }
1426
1463
 
1427
1464
  .header-content {
1428
1465
  display: flex;
1429
1466
  align-items: center;
1430
- justify-content: space-between;
1431
1467
  overflow: hidden;
1432
1468
  flex-grow: 1;
1433
1469
  min-width: 0;
@@ -1435,7 +1471,6 @@
1435
1471
  overflow-wrap: break-word;
1436
1472
  word-break: normal;
1437
1473
  height: 100%;
1438
- padding: var(--size-1);
1439
1474
  gap: var(--size-1);
1440
1475
  }
1441
1476
 
@@ -1465,7 +1500,8 @@
1465
1500
  transform: translateY(-50%);
1466
1501
  }
1467
1502
 
1468
- .cell-selected .cell-menu-button {
1503
+ .cell-selected .cell-menu-button,
1504
+ th:hover .cell-menu-button {
1469
1505
  display: flex;
1470
1506
  align-items: center;
1471
1507
  justify-content: center;
@@ -1682,4 +1718,24 @@
1682
1718
  .always-frozen {
1683
1719
  z-index: var(--layer-3);
1684
1720
  }
1721
+
1722
+ .add-row-container {
1723
+ margin-top: var(--size-2);
1724
+ }
1725
+
1726
+ .add-row-button {
1727
+ width: 100%;
1728
+ padding: var(--size-1);
1729
+ background: transparent;
1730
+ border: 1px dashed var(--border-color-primary);
1731
+ border-radius: var(--radius-sm);
1732
+ color: var(--body-text-color);
1733
+ cursor: pointer;
1734
+ transition: all 150ms;
1735
+ }
1736
+
1737
+ .add-row-button:hover {
1738
+ background: var(--background-fill-secondary);
1739
+ border-style: solid;
1740
+ }
1685
1741
  </style>