@gradio/dataframe 0.17.13 → 0.17.15

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,30 @@
1
1
  # @gradio/dataframe
2
2
 
3
+ ## 0.17.15
4
+
5
+ ### Features
6
+
7
+ - [#11237](https://github.com/gradio-app/gradio/pull/11237) [`a6f6b40`](https://github.com/gradio-app/gradio/commit/a6f6b40dda5194fa5bc9926ef67f2a75f503e9a4) - Enhance boolean cell types in `gr.Dataframe`. Thanks @hannahblair!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/upload@0.16.7
12
+ - @gradio/checkbox@0.4.23
13
+ - @gradio/client@1.15.2
14
+ - @gradio/button@0.5.3
15
+
16
+ ## 0.17.14
17
+
18
+ ### Fixes
19
+
20
+ - [#11282](https://github.com/gradio-app/gradio/pull/11282) [`9a6a6f5`](https://github.com/gradio-app/gradio/commit/9a6a6f524e8e9784144d45ea7b6327a1a7b3593c) - Fix DataFrame Group Flicker. Thanks @deckar01!
21
+
22
+ ### Dependency updates
23
+
24
+ - @gradio/statustracker@0.10.12
25
+ - @gradio/markdown-code@0.4.3
26
+ - @gradio/button@0.5.2
27
+
3
28
  ## 0.17.13
4
29
 
5
30
  ### Dependency updates
@@ -60,11 +60,12 @@
60
60
  name="Interactive dataframe with label"
61
61
  args={{
62
62
  values: [
63
- ["Cat", 5],
64
- ["Horse", 3],
65
- ["Snake", 1]
63
+ ["Cat", 5, true],
64
+ ["Horse", 3, false],
65
+ ["Snake", 1, false]
66
66
  ],
67
- headers: ["Animal", "Votes"],
67
+ headers: ["Animal", "Votes", "Is Pet"],
68
+ datatype: ["str", "number", "bool"],
68
69
  label: "Animals",
69
70
  show_label: true,
70
71
  col_count: [2, "dynamic"],
@@ -93,14 +94,14 @@
93
94
  name="Static dataframe"
94
95
  args={{
95
96
  values: [
96
- ["Cat", 5],
97
- ["Horse", 3],
98
- ["Snake", 1]
97
+ ["Cat", 5, true],
98
+ ["Horse", 3, false],
99
+ ["Snake", 1, false]
99
100
  ],
100
- headers: ["Animal", "Votes"],
101
-
101
+ headers: ["Animal", "Votes", "Is Pet"],
102
+ datatype: ["str", "number", "bool"],
102
103
  label: "Animals",
103
- col_count: [2, "dynamic"],
104
+ col_count: [3, "dynamic"],
104
105
  row_count: [3, "dynamic"],
105
106
  editable: false
106
107
  }}
@@ -0,0 +1,62 @@
1
+ <script>import { BaseCheckbox } from "@gradio/checkbox";
2
+ export let value = false;
3
+ export let editable = true;
4
+ export let on_change;
5
+ $:
6
+ bool_value = typeof value === "string" ? value.toLowerCase() === "true" : !!value;
7
+ function handle_change(event) {
8
+ on_change(event.detail);
9
+ }
10
+ function handle_click(event) {
11
+ event.stopPropagation();
12
+ }
13
+ function handle_keydown(event) {
14
+ if (event.key === "Enter" || event.key === " ") {
15
+ event.stopPropagation();
16
+ }
17
+ }
18
+ </script>
19
+
20
+ <div
21
+ class="bool-cell checkbox"
22
+ on:click={handle_click}
23
+ on:keydown={handle_keydown}
24
+ role="button"
25
+ tabindex="-1"
26
+ >
27
+ <BaseCheckbox
28
+ bind:value={bool_value}
29
+ label=""
30
+ interactive={editable}
31
+ on:change={handle_change}
32
+ />
33
+ </div>
34
+
35
+ <style>
36
+ .bool-cell {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ width: var(--size-full);
41
+ height: var(--size-full);
42
+ }
43
+ .bool-cell :global(input:disabled) {
44
+ opacity: 0.8;
45
+ }
46
+
47
+ .bool-cell.checkbox {
48
+ justify-content: center;
49
+ }
50
+
51
+ .bool-cell :global(label) {
52
+ margin: 0;
53
+ width: 100%;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: center;
57
+ }
58
+
59
+ .bool-cell :global(span) {
60
+ display: none;
61
+ }
62
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ value?: (boolean | string) | undefined;
5
+ editable?: boolean | undefined;
6
+ on_change: (value: boolean) => void;
7
+ };
8
+ events: {
9
+ [evt: string]: CustomEvent<any>;
10
+ };
11
+ slots: {};
12
+ };
13
+ export type BooleanCellProps = typeof __propDef.props;
14
+ export type BooleanCellEvents = typeof __propDef.events;
15
+ export type BooleanCellSlots = typeof __propDef.slots;
16
+ export default class BooleanCell extends SvelteComponent<BooleanCellProps, BooleanCellEvents, BooleanCellSlots> {
17
+ }
18
+ export {};
@@ -1,6 +1,7 @@
1
1
  <script>import { createEventDispatcher } from "svelte";
2
2
  import { MarkdownCode } from "@gradio/markdown-code";
3
3
  import SelectionButtons from "./icons/SelectionButtons.svelte";
4
+ import BooleanCell from "./BooleanCell.svelte";
4
5
  export let edit;
5
6
  export let value = "";
6
7
  export let display_value = null;
@@ -21,9 +22,9 @@ export let show_selection_buttons = false;
21
22
  export let coords;
22
23
  export let on_select_column = null;
23
24
  export let on_select_row = null;
25
+ export let el;
24
26
  const dispatch = createEventDispatcher();
25
27
  let is_expanded = false;
26
- export let el;
27
28
  function truncate_text(text, max_length = null, is_image = false) {
28
29
  if (is_image)
29
30
  return String(text);
@@ -65,9 +66,22 @@ function handle_click() {
65
66
  is_expanded = !is_expanded;
66
67
  }
67
68
  }
69
+ function handle_bool_change(new_value) {
70
+ value = new_value.toString();
71
+ dispatch("blur", {
72
+ blur_event: {
73
+ target: {
74
+ type: "checkbox",
75
+ checked: new_value,
76
+ value: new_value.toString()
77
+ }
78
+ },
79
+ coords
80
+ });
81
+ }
68
82
  </script>
69
83
 
70
- {#if edit}
84
+ {#if edit && datatype !== "bool"}
71
85
  <input
72
86
  readonly={is_static}
73
87
  aria-readonly={is_static}
@@ -86,48 +100,57 @@ function handle_click() {
86
100
  />
87
101
  {/if}
88
102
 
89
- <span
90
- class:dragging={is_dragging}
91
- on:click={handle_click}
92
- on:keydown={handle_keydown}
93
- tabindex="0"
94
- role="button"
95
- class:edit
96
- class:expanded={is_expanded}
97
- class:multiline={header}
98
- on:focus|preventDefault
99
- style={styling}
100
- data-editable={editable}
101
- data-max-chars={max_chars}
102
- data-expanded={is_expanded}
103
- placeholder=" "
104
- class:text={datatype === "str"}
105
- class:wrap={wrap_text}
106
- >
107
- {#if datatype === "image" && components.image}
108
- <svelte:component
109
- this={components.image}
110
- value={{ url: display_text }}
111
- show_label={false}
112
- label="cell-image"
113
- show_download_button={false}
114
- {i18n}
115
- gradio={{ dispatch: () => {} }}
116
- />
117
- {:else if datatype === "html"}
118
- {@html display_text}
119
- {:else if datatype === "markdown"}
120
- <MarkdownCode
121
- message={display_text.toLocaleString()}
122
- {latex_delimiters}
123
- {line_breaks}
124
- chatbot={false}
125
- {root}
126
- />
127
- {:else}
128
- {display_text}
129
- {/if}
130
- </span>
103
+ {#if datatype === "bool"}
104
+ <BooleanCell
105
+ value={String(display_content)}
106
+ {editable}
107
+ on_change={handle_bool_change}
108
+ />
109
+ {:else}
110
+ <span
111
+ class:dragging={is_dragging}
112
+ on:click={handle_click}
113
+ on:keydown={handle_keydown}
114
+ tabindex="0"
115
+ role="button"
116
+ class:edit
117
+ class:expanded={is_expanded}
118
+ class:multiline={header}
119
+ on:focus|preventDefault
120
+ style={styling}
121
+ data-editable={editable}
122
+ data-max-chars={max_chars}
123
+ data-expanded={is_expanded}
124
+ placeholder=" "
125
+ class:text={datatype === "str"}
126
+ class:wrap={wrap_text}
127
+ >
128
+ {#if datatype === "image" && components.image}
129
+ <svelte:component
130
+ this={components.image}
131
+ value={{ url: display_text }}
132
+ show_label={false}
133
+ label="cell-image"
134
+ show_download_button={false}
135
+ {i18n}
136
+ gradio={{ dispatch: () => {} }}
137
+ />
138
+ {:else if datatype === "html"}
139
+ {@html display_text}
140
+ {:else if datatype === "markdown"}
141
+ <MarkdownCode
142
+ message={display_text.toLocaleString()}
143
+ {latex_delimiters}
144
+ {line_breaks}
145
+ chatbot={false}
146
+ {root}
147
+ />
148
+ {:else}
149
+ {display_text}
150
+ {/if}
151
+ </span>
152
+ {/if}
153
+
131
154
  {#if show_selection_buttons && coords && on_select_column && on_select_row}
132
155
  <SelectionButtons
133
156
  position="column"
@@ -26,15 +26,6 @@ export let is_header = false;
26
26
  text-overflow: ellipsis;
27
27
  white-space: nowrap;
28
28
  font-weight: var(--weight-semibold);
29
- background: var(--table-even-background-fill);
30
29
  border-right: 1px solid var(--border-color-primary);
31
30
  }
32
-
33
- :global(tr:nth-child(odd)) .row-number {
34
- background: var(--table-odd-background-fill);
35
- }
36
-
37
- :global(tr:nth-child(even)) .row-number {
38
- background: var(--table-even-background-fill);
39
- }
40
31
  </style>
@@ -103,7 +103,7 @@ onMount(() => {
103
103
  const observer = new IntersectionObserver((entries) => {
104
104
  entries.forEach((entry) => {
105
105
  if (entry.isIntersecting && !is_visible) {
106
- set_cell_widths();
106
+ width_calculated = false;
107
107
  }
108
108
  is_visible = entry.isIntersecting;
109
109
  });
@@ -111,10 +111,17 @@ onMount(() => {
111
111
  observer.observe(parent);
112
112
  document.addEventListener("click", handle_click_outside);
113
113
  window.addEventListener("resize", handle_resize);
114
+ const global_mouse_up = (event) => {
115
+ if (is_dragging || drag_start) {
116
+ handle_mouse_up(event);
117
+ }
118
+ };
119
+ document.addEventListener("mouseup", global_mouse_up);
114
120
  return () => {
115
121
  observer.disconnect();
116
122
  document.removeEventListener("click", handle_click_outside);
117
123
  window.removeEventListener("resize", handle_resize);
124
+ document.removeEventListener("mouseup", global_mouse_up);
118
125
  };
119
126
  });
120
127
  $: {
@@ -170,6 +177,7 @@ $:
170
177
  }
171
178
  last_width_data_length = 0;
172
179
  last_width_column_count = 0;
180
+ width_calculated = false;
173
181
  }
174
182
  }
175
183
  const is_reset = values.length === 0 || values.length === 1 && values[0].length === 0;
@@ -193,8 +201,8 @@ $:
193
201
  if ($df_state.current_search_query) {
194
202
  df_actions.handle_search(null);
195
203
  }
196
- if (parent && cells.length > 0) {
197
- set_cell_widths();
204
+ if (parent && cells.length > 0 && (is_reset || is_different_structure)) {
205
+ width_calculated = false;
198
206
  }
199
207
  }
200
208
  $:
@@ -328,8 +336,18 @@ function handle_click_outside(event) {
328
336
  }
329
337
  $:
330
338
  max = get_max(data);
339
+ let width_calc_timeout;
340
+ $:
341
+ if (cells[0] && cells[0]?.clientWidth) {
342
+ clearTimeout(width_calc_timeout);
343
+ width_calc_timeout = setTimeout(() => set_cell_widths(), 100);
344
+ }
345
+ let width_calculated = false;
331
346
  $:
332
- cells[0] && cells[0]?.clientWidth && set_cell_widths();
347
+ if (cells[0] && !width_calculated) {
348
+ set_cell_widths();
349
+ width_calculated = true;
350
+ }
333
351
  let cells = [];
334
352
  let parent;
335
353
  let table;
@@ -512,6 +530,7 @@ function handle_resize() {
512
530
  selected_cells = [];
513
531
  selected = false;
514
532
  editing = false;
533
+ width_calculated = false;
515
534
  set_cell_widths();
516
535
  }
517
536
  function add_row_at(index, position) {
@@ -945,7 +964,11 @@ function get_cell_display_value(row, col) {
945
964
  overflow-x: hidden;
946
965
  }
947
966
 
948
- .row-odd {
967
+ tr {
968
+ background: var(--table-even-background-fill);
969
+ }
970
+
971
+ tr.row-odd {
949
972
  background: var(--table-odd-background-fill);
950
973
  }
951
974
 
@@ -333,10 +333,6 @@ onMount(() => {
333
333
  scroll-snap-align: start;
334
334
  }
335
335
 
336
- tbody > :global(tr:nth-child(even)) {
337
- background: var(--table-even-background-fill);
338
- }
339
-
340
336
  tbody :global(td.pinned-column) {
341
337
  position: sticky;
342
338
  z-index: 3;
@@ -43,6 +43,10 @@ export function create_drag_handlers(state, set_is_dragging, set_selected_cells,
43
43
  handle_mouse_move(event) {
44
44
  if (!state.drag_start || !state.mouse_down_pos)
45
45
  return;
46
+ if (!(event.buttons & 1)) {
47
+ end_drag(event);
48
+ return;
49
+ }
46
50
  const dx = Math.abs(event.clientX - state.mouse_down_pos.x);
47
51
  const dy = Math.abs(event.clientY - state.mouse_down_pos.y);
48
52
  if (!state.is_dragging && (dx > 3 || dy > 3)) {
@@ -23,7 +23,7 @@ export async function handle_cell_blur(event, ctx, coords) {
23
23
  const input_el = event.target;
24
24
  if (!input_el || input_el.value === undefined)
25
25
  return;
26
- await save_cell_value(input_el.value, ctx, coords[0], coords[1]);
26
+ await save_cell_value(input_el.type === "checkbox" ? String(input_el.checked) : input_el.value, ctx, coords[0], coords[1]);
27
27
  }
28
28
  function handle_header_navigation(event, ctx) {
29
29
  const state = get(ctx.state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/dataframe",
3
- "version": "0.17.13",
3
+ "version": "0.17.15",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -17,14 +17,15 @@
17
17
  "dompurify": "^3.0.3",
18
18
  "katex": "^0.16.7",
19
19
  "marked": "^12.0.0",
20
+ "@gradio/button": "^0.5.3",
20
21
  "@gradio/atoms": "^0.16.1",
21
- "@gradio/button": "^0.5.1",
22
+ "@gradio/checkbox": "^0.4.23",
23
+ "@gradio/client": "^1.15.2",
22
24
  "@gradio/icons": "^0.12.0",
23
- "@gradio/markdown-code": "^0.4.3",
24
- "@gradio/client": "^1.15.1",
25
25
  "@gradio/statustracker": "^0.10.12",
26
- "@gradio/upload": "^0.16.6",
27
- "@gradio/utils": "^0.10.2"
26
+ "@gradio/upload": "^0.16.7",
27
+ "@gradio/utils": "^0.10.2",
28
+ "@gradio/markdown-code": "^0.4.3"
28
29
  },
29
30
  "exports": {
30
31
  ".": {
@@ -43,7 +44,7 @@
43
44
  "svelte": "^4.0.0"
44
45
  },
45
46
  "devDependencies": {
46
- "@gradio/preview": "^0.13.0"
47
+ "@gradio/preview": "^0.13.1"
47
48
  },
48
49
  "repository": {
49
50
  "type": "git",
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import { BaseCheckbox } from "@gradio/checkbox";
3
+
4
+ export let value: boolean | string = false;
5
+ export let editable = true;
6
+ export let on_change: (value: boolean) => void;
7
+
8
+ $: bool_value =
9
+ typeof value === "string" ? value.toLowerCase() === "true" : !!value;
10
+
11
+ function handle_change(event: CustomEvent<boolean>): void {
12
+ on_change(event.detail);
13
+ }
14
+
15
+ function handle_click(event: MouseEvent): void {
16
+ event.stopPropagation();
17
+ }
18
+
19
+ function handle_keydown(event: KeyboardEvent): void {
20
+ if (event.key === "Enter" || event.key === " ") {
21
+ event.stopPropagation();
22
+ }
23
+ }
24
+ </script>
25
+
26
+ <div
27
+ class="bool-cell checkbox"
28
+ on:click={handle_click}
29
+ on:keydown={handle_keydown}
30
+ role="button"
31
+ tabindex="-1"
32
+ >
33
+ <BaseCheckbox
34
+ bind:value={bool_value}
35
+ label=""
36
+ interactive={editable}
37
+ on:change={handle_change}
38
+ />
39
+ </div>
40
+
41
+ <style>
42
+ .bool-cell {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: var(--size-full);
47
+ height: var(--size-full);
48
+ }
49
+ .bool-cell :global(input:disabled) {
50
+ opacity: 0.8;
51
+ }
52
+
53
+ .bool-cell.checkbox {
54
+ justify-content: center;
55
+ }
56
+
57
+ .bool-cell :global(label) {
58
+ margin: 0;
59
+ width: 100%;
60
+ display: flex;
61
+ justify-content: center;
62
+ align-items: center;
63
+ }
64
+
65
+ .bool-cell :global(span) {
66
+ display: none;
67
+ }
68
+ </style>
@@ -3,6 +3,8 @@
3
3
  import { MarkdownCode } from "@gradio/markdown-code";
4
4
  import type { I18nFormatter } from "@gradio/utils";
5
5
  import SelectionButtons from "./icons/SelectionButtons.svelte";
6
+ import BooleanCell from "./BooleanCell.svelte";
7
+
6
8
  export let edit: boolean;
7
9
  export let value: string | number = "";
8
10
  export let display_value: string | null = null;
@@ -35,6 +37,7 @@
35
37
  export let coords: [number, number];
36
38
  export let on_select_column: ((col: number) => void) | null = null;
37
39
  export let on_select_row: ((row: number) => void) | null = null;
40
+ export let el: HTMLInputElement | null;
38
41
 
39
42
  const dispatch = createEventDispatcher<{
40
43
  blur: { blur_event: FocusEvent; coords: [number, number] };
@@ -43,8 +46,6 @@
43
46
 
44
47
  let is_expanded = false;
45
48
 
46
- export let el: HTMLInputElement | null;
47
-
48
49
  function truncate_text(
49
50
  text: string | number,
50
51
  max_length: number | null = null,
@@ -99,9 +100,23 @@
99
100
  is_expanded = !is_expanded;
100
101
  }
101
102
  }
103
+
104
+ function handle_bool_change(new_value: boolean): void {
105
+ value = new_value.toString();
106
+ dispatch("blur", {
107
+ blur_event: {
108
+ target: {
109
+ type: "checkbox",
110
+ checked: new_value,
111
+ value: new_value.toString()
112
+ }
113
+ } as unknown as FocusEvent,
114
+ coords: coords
115
+ });
116
+ }
102
117
  </script>
103
118
 
104
- {#if edit}
119
+ {#if edit && datatype !== "bool"}
105
120
  <input
106
121
  readonly={is_static}
107
122
  aria-readonly={is_static}
@@ -120,48 +135,57 @@
120
135
  />
121
136
  {/if}
122
137
 
123
- <span
124
- class:dragging={is_dragging}
125
- on:click={handle_click}
126
- on:keydown={handle_keydown}
127
- tabindex="0"
128
- role="button"
129
- class:edit
130
- class:expanded={is_expanded}
131
- class:multiline={header}
132
- on:focus|preventDefault
133
- style={styling}
134
- data-editable={editable}
135
- data-max-chars={max_chars}
136
- data-expanded={is_expanded}
137
- placeholder=" "
138
- class:text={datatype === "str"}
139
- class:wrap={wrap_text}
140
- >
141
- {#if datatype === "image" && components.image}
142
- <svelte:component
143
- this={components.image}
144
- value={{ url: display_text }}
145
- show_label={false}
146
- label="cell-image"
147
- show_download_button={false}
148
- {i18n}
149
- gradio={{ dispatch: () => {} }}
150
- />
151
- {:else if datatype === "html"}
152
- {@html display_text}
153
- {:else if datatype === "markdown"}
154
- <MarkdownCode
155
- message={display_text.toLocaleString()}
156
- {latex_delimiters}
157
- {line_breaks}
158
- chatbot={false}
159
- {root}
160
- />
161
- {:else}
162
- {display_text}
163
- {/if}
164
- </span>
138
+ {#if datatype === "bool"}
139
+ <BooleanCell
140
+ value={String(display_content)}
141
+ {editable}
142
+ on_change={handle_bool_change}
143
+ />
144
+ {:else}
145
+ <span
146
+ class:dragging={is_dragging}
147
+ on:click={handle_click}
148
+ on:keydown={handle_keydown}
149
+ tabindex="0"
150
+ role="button"
151
+ class:edit
152
+ class:expanded={is_expanded}
153
+ class:multiline={header}
154
+ on:focus|preventDefault
155
+ style={styling}
156
+ data-editable={editable}
157
+ data-max-chars={max_chars}
158
+ data-expanded={is_expanded}
159
+ placeholder=" "
160
+ class:text={datatype === "str"}
161
+ class:wrap={wrap_text}
162
+ >
163
+ {#if datatype === "image" && components.image}
164
+ <svelte:component
165
+ this={components.image}
166
+ value={{ url: display_text }}
167
+ show_label={false}
168
+ label="cell-image"
169
+ show_download_button={false}
170
+ {i18n}
171
+ gradio={{ dispatch: () => {} }}
172
+ />
173
+ {:else if datatype === "html"}
174
+ {@html display_text}
175
+ {:else if datatype === "markdown"}
176
+ <MarkdownCode
177
+ message={display_text.toLocaleString()}
178
+ {latex_delimiters}
179
+ {line_breaks}
180
+ chatbot={false}
181
+ {root}
182
+ />
183
+ {:else}
184
+ {display_text}
185
+ {/if}
186
+ </span>
187
+ {/if}
188
+
165
189
  {#if show_selection_buttons && coords && on_select_column && on_select_row}
166
190
  <SelectionButtons
167
191
  position="column"
@@ -27,15 +27,6 @@
27
27
  text-overflow: ellipsis;
28
28
  white-space: nowrap;
29
29
  font-weight: var(--weight-semibold);
30
- background: var(--table-even-background-fill);
31
30
  border-right: 1px solid var(--border-color-primary);
32
31
  }
33
-
34
- :global(tr:nth-child(odd)) .row-number {
35
- background: var(--table-odd-background-fill);
36
- }
37
-
38
- :global(tr:nth-child(even)) .row-number {
39
- background: var(--table-even-background-fill);
40
- }
41
32
  </style>
@@ -120,7 +120,7 @@
120
120
  const observer = new IntersectionObserver((entries) => {
121
121
  entries.forEach((entry) => {
122
122
  if (entry.isIntersecting && !is_visible) {
123
- set_cell_widths();
123
+ width_calculated = false;
124
124
  }
125
125
  is_visible = entry.isIntersecting;
126
126
  });
@@ -129,10 +129,18 @@
129
129
  document.addEventListener("click", handle_click_outside);
130
130
  window.addEventListener("resize", handle_resize);
131
131
 
132
+ const global_mouse_up = (event: MouseEvent): void => {
133
+ if (is_dragging || drag_start) {
134
+ handle_mouse_up(event);
135
+ }
136
+ };
137
+ document.addEventListener("mouseup", global_mouse_up);
138
+
132
139
  return () => {
133
140
  observer.disconnect();
134
141
  document.removeEventListener("click", handle_click_outside);
135
142
  window.removeEventListener("resize", handle_resize);
143
+ document.removeEventListener("mouseup", global_mouse_up);
136
144
  };
137
145
  });
138
146
 
@@ -223,6 +231,7 @@
223
231
  }
224
232
  last_width_data_length = 0;
225
233
  last_width_column_count = 0;
234
+ width_calculated = false;
226
235
  }
227
236
  }
228
237
 
@@ -256,8 +265,8 @@
256
265
  df_actions.handle_search(null);
257
266
  }
258
267
 
259
- if (parent && cells.length > 0) {
260
- set_cell_widths();
268
+ if (parent && cells.length > 0 && (is_reset || is_different_structure)) {
269
+ width_calculated = false;
261
270
  }
262
271
  }
263
272
 
@@ -421,9 +430,17 @@
421
430
 
422
431
  $: max = get_max(data);
423
432
 
424
- // Modify how we trigger cell width calculations
425
- // Only recalculate when cells actually change, not during sort
426
- $: cells[0] && cells[0]?.clientWidth && set_cell_widths();
433
+ let width_calc_timeout: ReturnType<typeof setTimeout>;
434
+ $: if (cells[0] && cells[0]?.clientWidth) {
435
+ clearTimeout(width_calc_timeout);
436
+ width_calc_timeout = setTimeout(() => set_cell_widths(), 100);
437
+ }
438
+
439
+ let width_calculated = false;
440
+ $: if (cells[0] && !width_calculated) {
441
+ set_cell_widths();
442
+ width_calculated = true;
443
+ }
427
444
  let cells: HTMLTableCellElement[] = [];
428
445
  let parent: HTMLDivElement;
429
446
  let table: HTMLTableElement;
@@ -649,6 +666,7 @@
649
666
  selected_cells = [];
650
667
  selected = false;
651
668
  editing = false;
669
+ width_calculated = false;
652
670
  set_cell_widths();
653
671
  }
654
672
 
@@ -1092,7 +1110,11 @@
1092
1110
  overflow-x: hidden;
1093
1111
  }
1094
1112
 
1095
- .row-odd {
1113
+ tr {
1114
+ background: var(--table-even-background-fill);
1115
+ }
1116
+
1117
+ tr.row-odd {
1096
1118
  background: var(--table-odd-background-fill);
1097
1119
  }
1098
1120
 
@@ -396,10 +396,6 @@
396
396
  scroll-snap-align: start;
397
397
  }
398
398
 
399
- tbody > :global(tr:nth-child(even)) {
400
- background: var(--table-even-background-fill);
401
- }
402
-
403
399
  tbody :global(td.pinned-column) {
404
400
  position: sticky;
405
401
  z-index: 3;
@@ -75,6 +75,11 @@ export function create_drag_handlers(
75
75
  handle_mouse_move(event: MouseEvent): void {
76
76
  if (!state.drag_start || !state.mouse_down_pos) return;
77
77
 
78
+ if (!(event.buttons & 1)) {
79
+ end_drag(event);
80
+ return;
81
+ }
82
+
78
83
  const dx = Math.abs(event.clientX - state.mouse_down_pos.x);
79
84
  const dy = Math.abs(event.clientY - state.mouse_down_pos.y);
80
85
 
@@ -37,7 +37,12 @@ export async function handle_cell_blur(
37
37
  const input_el = event.target as HTMLInputElement;
38
38
  if (!input_el || input_el.value === undefined) return;
39
39
 
40
- await save_cell_value(input_el.value, ctx, coords[0], coords[1]);
40
+ await save_cell_value(
41
+ input_el.type === "checkbox" ? String(input_el.checked) : input_el.value,
42
+ ctx,
43
+ coords[0],
44
+ coords[1]
45
+ );
41
46
  }
42
47
 
43
48
  function handle_header_navigation(