@gradio/dataframe 0.14.0 → 0.15.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.
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { afterUpdate, createEventDispatcher, tick, onMount } from "svelte";
3
- import { dsvFormat } from "d3-dsv";
4
3
  import { dequal } from "dequal/lite";
5
4
  import { Upload } from "@gradio/upload";
6
5
 
@@ -17,7 +16,19 @@
17
16
  } from "./utils";
18
17
  import CellMenu from "./CellMenu.svelte";
19
18
  import Toolbar from "./Toolbar.svelte";
20
- import { copy_table_data } from "./table_utils";
19
+ import type { CellCoordinate, EditingState } from "./types";
20
+ import {
21
+ is_cell_selected,
22
+ handle_selection,
23
+ handle_delete_key,
24
+ should_show_cell_menu,
25
+ get_next_cell_coordinates,
26
+ get_range_selection,
27
+ move_cursor,
28
+ get_current_indices,
29
+ handle_click_outside as handle_click_outside_util
30
+ } from "./selection_utils";
31
+ import { copy_table_data, get_max, handle_file_upload } from "./table_utils";
21
32
 
22
33
  export let datatype: Datatype | Datatype[];
23
34
  export let label: string | null = null;
@@ -46,12 +57,24 @@
46
57
  export let show_fullscreen_button = false;
47
58
  export let show_copy_button = false;
48
59
  export let value_is_output = false;
60
+ export let max_chars: number | undefined = undefined;
61
+
62
+ let selected_cells: CellCoordinate[] = [];
63
+ $: selected_cells = [...selected_cells];
64
+ let selected: CellCoordinate | false = false;
65
+ $: selected =
66
+ selected_cells.length > 0
67
+ ? selected_cells[selected_cells.length - 1]
68
+ : false;
49
69
 
50
- let selected: false | [number, number] = false;
51
- let clicked_cell: { row: number; col: number } | undefined = undefined;
52
70
  export let display_value: string[][] | null = null;
53
71
  export let styling: string[][] | null = null;
54
72
  let t_rect: DOMRectReadOnly;
73
+ let els: Record<
74
+ string,
75
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
76
+ > = {};
77
+ let data_binding: Record<string, (typeof data)[0][0]> = {};
55
78
 
56
79
  const dispatch = createEventDispatcher<{
57
80
  change: DataframeValue;
@@ -59,34 +82,27 @@
59
82
  select: SelectData;
60
83
  }>();
61
84
 
62
- let editing: false | [number, number] = false;
85
+ let editing: EditingState = false;
86
+ let clear_on_focus = false;
87
+ let header_edit: number | false = false;
88
+ let selected_header: number | false = false;
89
+ let active_cell_menu: {
90
+ row: number;
91
+ col: number;
92
+ x: number;
93
+ y: number;
94
+ } | null = null;
95
+ let active_header_menu: {
96
+ col: number;
97
+ x: number;
98
+ y: number;
99
+ } | null = null;
100
+ let is_fullscreen = false;
101
+ let dragging = false;
63
102
 
64
103
  const get_data_at = (row: number, col: number): string | number =>
65
104
  data?.[row]?.[col]?.value;
66
105
 
67
- let last_selected: [number, number] | null = null;
68
-
69
- $: {
70
- if (selected !== false && !dequal(selected, last_selected)) {
71
- const [row, col] = selected;
72
- if (!isNaN(row) && !isNaN(col) && data[row]) {
73
- dispatch("select", {
74
- index: [row, col],
75
- value: get_data_at(row, col),
76
- row_value: data[row].map((d) => d.value)
77
- });
78
- last_selected = selected;
79
- }
80
- }
81
- }
82
-
83
- let els: Record<
84
- string,
85
- { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
86
- > = {};
87
-
88
- let data_binding: Record<string, (typeof data)[0][0]> = {};
89
-
90
106
  function make_id(): string {
91
107
  return Math.random().toString(36).substring(2, 15);
92
108
  }
@@ -199,48 +215,9 @@
199
215
  if (direction === "asc") return "ascending";
200
216
  if (direction === "des") return "descending";
201
217
  }
202
-
203
218
  return "none";
204
219
  }
205
220
 
206
- function get_current_indices(id: string): [number, number] {
207
- return data.reduce(
208
- (acc, arr, i) => {
209
- const j = arr.reduce(
210
- (_acc, _data, k) => (id === _data.id ? k : _acc),
211
- -1
212
- );
213
-
214
- return j === -1 ? acc : [i, j];
215
- },
216
- [-1, -1]
217
- );
218
- }
219
-
220
- function move_cursor(
221
- key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
222
- current_coords: [number, number]
223
- ): void {
224
- const dir = {
225
- ArrowRight: [0, 1],
226
- ArrowLeft: [0, -1],
227
- ArrowDown: [1, 0],
228
- ArrowUp: [-1, 0]
229
- }[key];
230
-
231
- const i = current_coords[0] + dir[0];
232
- const j = current_coords[1] + dir[1];
233
-
234
- if (i < 0 && j <= 0) {
235
- selected_header = j;
236
- selected = false;
237
- } else {
238
- const is_data = data[i]?.[j];
239
- selected = is_data ? [i, j] : selected;
240
- }
241
- }
242
-
243
- let clear_on_focus = false;
244
221
  // eslint-disable-next-line complexity
245
222
  async function handle_keydown(event: KeyboardEvent): Promise<void> {
246
223
  if (selected_header !== false && header_edit === false) {
@@ -268,6 +245,50 @@
268
245
  break;
269
246
  }
270
247
  }
248
+
249
+ if (event.key === "Delete" || event.key === "Backspace") {
250
+ if (!editable) return;
251
+
252
+ if (editing) {
253
+ const [row, col] = editing;
254
+ const input_el = els[data[row][col].id].input;
255
+ if (input_el && input_el.selectionStart !== input_el.selectionEnd) {
256
+ return;
257
+ }
258
+ if (
259
+ event.key === "Delete" &&
260
+ input_el?.selectionStart !== input_el?.value.length
261
+ ) {
262
+ return;
263
+ }
264
+ if (event.key === "Backspace" && input_el?.selectionStart !== 0) {
265
+ return;
266
+ }
267
+ }
268
+
269
+ event.preventDefault();
270
+ if (selected_cells.length > 0) {
271
+ data = handle_delete_key(data, selected_cells);
272
+ dispatch("change", {
273
+ data: data.map((row) => row.map((cell) => cell.value)),
274
+ headers: _headers.map((h) => h.value),
275
+ metadata: null
276
+ });
277
+ if (!value_is_output) {
278
+ dispatch("input");
279
+ }
280
+ }
281
+ return;
282
+ }
283
+
284
+ if (event.key === "c" && (event.metaKey || event.ctrlKey)) {
285
+ event.preventDefault();
286
+ if (selected_cells.length > 0) {
287
+ await handle_copy();
288
+ }
289
+ return;
290
+ }
291
+
271
292
  if (!selected) {
272
293
  return;
273
294
  }
@@ -281,7 +302,29 @@
281
302
  case "ArrowUp":
282
303
  if (editing) break;
283
304
  event.preventDefault();
284
- move_cursor(event.key, [i, j]);
305
+ const next_coords = move_cursor(event.key, [i, j], data);
306
+ if (next_coords) {
307
+ if (event.shiftKey) {
308
+ selected_cells = get_range_selection(
309
+ selected_cells.length > 0 ? selected_cells[0] : [i, j],
310
+ next_coords
311
+ );
312
+ editing = false;
313
+ } else {
314
+ selected_cells = [next_coords];
315
+ editing = next_coords;
316
+ clear_on_focus = false;
317
+ }
318
+ selected = next_coords;
319
+ } else if (
320
+ next_coords === false &&
321
+ event.key === "ArrowUp" &&
322
+ i === 0
323
+ ) {
324
+ selected_header = j;
325
+ selected = false;
326
+ editing = false;
327
+ }
285
328
  break;
286
329
 
287
330
  case "Escape":
@@ -310,39 +353,26 @@
310
353
  selected = [i, j];
311
354
  } else {
312
355
  editing = [i, j];
356
+ clear_on_focus = false;
313
357
  }
314
358
  }
315
-
316
- break;
317
- case "Backspace":
318
- if (!editable) break;
319
- if (!editing) {
320
- event.preventDefault();
321
- data[i][j].value = "";
322
- }
323
- break;
324
- case "Delete":
325
- if (!editable) break;
326
- if (!editing) {
327
- event.preventDefault();
328
- data[i][j].value = "";
329
- }
330
359
  break;
331
360
  case "Tab":
332
- let direction = event.shiftKey ? -1 : 1;
333
-
334
- let is_data_x = data[i][j + direction];
335
- let is_data_y =
336
- data?.[i + direction]?.[direction > 0 ? 0 : _headers.length - 1];
337
-
338
- if (is_data_x || is_data_y) {
339
- event.preventDefault();
340
- selected = is_data_x
341
- ? [i, j + direction]
342
- : [i + direction, direction > 0 ? 0 : _headers.length - 1];
343
- }
361
+ event.preventDefault();
344
362
  editing = false;
345
-
363
+ const next_cell = get_next_cell_coordinates(
364
+ [i, j],
365
+ data,
366
+ event.shiftKey
367
+ );
368
+ if (next_cell) {
369
+ selected_cells = [next_cell];
370
+ selected = next_cell;
371
+ if (editable) {
372
+ editing = next_cell;
373
+ clear_on_focus = false;
374
+ }
375
+ }
346
376
  break;
347
377
  default:
348
378
  if (!editable) break;
@@ -373,16 +403,12 @@
373
403
  }
374
404
  }
375
405
 
376
- let header_edit: number | false;
377
-
378
- let select_on_focus = false;
379
- let selected_header: number | false = false;
380
406
  async function edit_header(i: number, _select = false): Promise<void> {
381
407
  if (!editable || col_count[1] !== "dynamic" || header_edit === i) return;
382
408
  selected = false;
409
+ selected_cells = [];
383
410
  selected_header = i;
384
411
  header_edit = i;
385
- select_on_focus = _select;
386
412
  }
387
413
 
388
414
  function end_header_edit(event: CustomEvent<KeyboardEvent>): void {
@@ -457,107 +483,14 @@
457
483
  }
458
484
 
459
485
  function handle_click_outside(event: Event): void {
460
- if (
461
- (active_cell_menu &&
462
- !(event.target as HTMLElement).closest(".cell-menu")) ||
463
- (active_header_menu &&
464
- !(event.target as HTMLElement).closest(".cell-menu"))
465
- ) {
486
+ if (handle_click_outside_util(event, parent)) {
487
+ editing = false;
488
+ selected_cells = [];
489
+ header_edit = false;
490
+ selected_header = false;
466
491
  active_cell_menu = null;
467
492
  active_header_menu = null;
468
493
  }
469
-
470
- const [trigger] = event.composedPath() as HTMLElement[];
471
- if (parent.contains(trigger)) {
472
- return;
473
- }
474
-
475
- clicked_cell = undefined;
476
- editing = false;
477
- selected = false;
478
- header_edit = false;
479
- selected_header = false;
480
- active_cell_menu = null;
481
- active_header_menu = null;
482
- }
483
-
484
- function guess_delimitaor(
485
- text: string,
486
- possibleDelimiters: string[]
487
- ): string[] {
488
- return possibleDelimiters.filter(weedOut);
489
-
490
- function weedOut(delimiter: string): boolean {
491
- var cache = -1;
492
- return text.split("\n").every(checkLength);
493
-
494
- function checkLength(line: string): boolean {
495
- if (!line) {
496
- return true;
497
- }
498
-
499
- var length = line.split(delimiter).length;
500
- if (cache < 0) {
501
- cache = length;
502
- }
503
- return cache === length && length > 1;
504
- }
505
- }
506
- }
507
-
508
- function data_uri_to_blob(data_uri: string): Blob {
509
- const byte_str = atob(data_uri.split(",")[1]);
510
- const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
511
-
512
- const ab = new ArrayBuffer(byte_str.length);
513
- const ia = new Uint8Array(ab);
514
-
515
- for (let i = 0; i < byte_str.length; i++) {
516
- ia[i] = byte_str.charCodeAt(i);
517
- }
518
-
519
- return new Blob([ab], { type: mime_str });
520
- }
521
-
522
- function blob_to_string(blob: Blob): void {
523
- const reader = new FileReader();
524
-
525
- function handle_read(e: ProgressEvent<FileReader>): void {
526
- if (!e?.target?.result || typeof e.target.result !== "string") return;
527
-
528
- const [delimiter] = guess_delimitaor(e.target.result, [",", "\t"]);
529
-
530
- const [head, ...rest] = dsvFormat(delimiter).parseRows(e.target.result);
531
-
532
- _headers = make_headers(
533
- col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head
534
- );
535
-
536
- values = rest;
537
- reader.removeEventListener("loadend", handle_read);
538
- }
539
-
540
- reader.addEventListener("loadend", handle_read);
541
-
542
- reader.readAsText(blob);
543
- }
544
-
545
- let dragging = false;
546
-
547
- function get_max(
548
- _d: { value: any; id: string }[][]
549
- ): { value: any; id: string }[] {
550
- if (!_d || _d.length === 0 || !_d[0]) return [];
551
- let max = _d[0].slice();
552
- for (let i = 0; i < _d.length; i++) {
553
- for (let j = 0; j < _d[i].length; j++) {
554
- if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
555
- max[j] = _d[i][j];
556
- }
557
- }
558
- }
559
-
560
- return max;
561
494
  }
562
495
 
563
496
  $: max = get_max(data);
@@ -628,7 +561,7 @@
628
561
  data = data;
629
562
 
630
563
  if (id) {
631
- const [i, j] = get_current_indices(id);
564
+ const [i, j] = get_current_indices(id, data);
632
565
  selected = [i, j];
633
566
  }
634
567
  }
@@ -640,19 +573,17 @@
640
573
  let is_visible = false;
641
574
 
642
575
  onMount(() => {
643
- const observer = new IntersectionObserver((entries, observer) => {
576
+ const observer = new IntersectionObserver((entries) => {
644
577
  entries.forEach((entry) => {
645
578
  if (entry.isIntersecting && !is_visible) {
646
579
  set_cell_widths();
647
580
  data = data;
648
581
  }
649
-
650
582
  is_visible = entry.isIntersecting;
651
583
  });
652
584
  });
653
585
 
654
586
  observer.observe(parent);
655
-
656
587
  document.addEventListener("click", handle_click_outside);
657
588
  window.addEventListener("resize", handle_resize);
658
589
  document.addEventListener("fullscreenchange", handle_fullscreen_change);
@@ -668,14 +599,43 @@
668
599
  };
669
600
  });
670
601
 
671
- let highlighted_column: number | null = null;
602
+ function handle_cell_click(
603
+ event: MouseEvent,
604
+ row: number,
605
+ col: number
606
+ ): void {
607
+ event.preventDefault();
608
+ event.stopPropagation();
609
+ clear_on_focus = false;
610
+ active_cell_menu = null;
611
+ active_header_menu = null;
612
+ selected_header = false;
613
+ header_edit = false;
672
614
 
673
- let active_cell_menu: {
674
- row: number;
675
- col: number;
676
- x: number;
677
- y: number;
678
- } | null = null;
615
+ selected_cells = handle_selection([row, col], selected_cells, event);
616
+
617
+ if (selected_cells.length === 1 && editable) {
618
+ editing = [row, col];
619
+ tick().then(() => {
620
+ const input_el = els[data[row][col].id].input;
621
+ if (input_el) {
622
+ input_el.focus();
623
+ input_el.selectionStart = input_el.selectionEnd =
624
+ input_el.value.length;
625
+ }
626
+ });
627
+ } else {
628
+ editing = false;
629
+ }
630
+
631
+ toggle_cell_button(row, col);
632
+
633
+ dispatch("select", {
634
+ index: [row, col],
635
+ value: get_data_at(row, col),
636
+ row_value: data[row].map((d) => d.value)
637
+ });
638
+ }
679
639
 
680
640
  function toggle_cell_menu(event: MouseEvent, row: number, col: number): void {
681
641
  event.stopPropagation();
@@ -689,12 +649,7 @@
689
649
  const cell = (event.target as HTMLElement).closest("td");
690
650
  if (cell) {
691
651
  const rect = cell.getBoundingClientRect();
692
- active_cell_menu = {
693
- row,
694
- col,
695
- x: rect.right,
696
- y: rect.bottom
697
- };
652
+ active_cell_menu = { row, col, x: rect.right, y: rect.bottom };
698
653
  }
699
654
  }
700
655
  }
@@ -726,33 +681,21 @@
726
681
  } | null = null;
727
682
 
728
683
  function toggle_header_button(col: number): void {
729
- if (active_button?.type === "header" && active_button.col === col) {
730
- active_button = null;
731
- } else {
732
- active_button = { type: "header", col };
733
- }
684
+ active_button =
685
+ active_button?.type === "header" && active_button.col === col
686
+ ? null
687
+ : { type: "header", col };
734
688
  }
735
689
 
736
690
  function toggle_cell_button(row: number, col: number): void {
737
- if (
691
+ active_button =
738
692
  active_button?.type === "cell" &&
739
693
  active_button.row === row &&
740
694
  active_button.col === col
741
- ) {
742
- active_button = null;
743
- } else {
744
- active_button = { type: "cell", row, col };
745
- }
695
+ ? null
696
+ : { type: "cell", row, col };
746
697
  }
747
698
 
748
- let active_header_menu: {
749
- col: number;
750
- x: number;
751
- y: number;
752
- } | null = null;
753
-
754
- let is_fullscreen = false;
755
-
756
699
  function toggle_fullscreen(): void {
757
700
  if (!document.fullscreenElement) {
758
701
  parent.requestFullscreen();
@@ -768,7 +711,7 @@
768
711
  }
769
712
 
770
713
  async function handle_copy(): Promise<void> {
771
- await copy_table_data(data, _headers);
714
+ await copy_table_data(data, _headers, selected_cells);
772
715
  }
773
716
 
774
717
  function toggle_header_menu(event: MouseEvent, col: number): void {
@@ -779,11 +722,7 @@
779
722
  const header = (event.target as HTMLElement).closest("th");
780
723
  if (header) {
781
724
  const rect = header.getBoundingClientRect();
782
- active_header_menu = {
783
- col,
784
- x: rect.right,
785
- y: rect.bottom
786
- };
725
+ active_header_menu = { col, x: rect.right, y: rect.bottom };
787
726
  }
788
727
  }
789
728
  }
@@ -884,6 +823,7 @@
884
823
  edit={false}
885
824
  el={null}
886
825
  {root}
826
+ {editable}
887
827
  />
888
828
 
889
829
  <div
@@ -919,6 +859,7 @@
919
859
  edit={false}
920
860
  el={null}
921
861
  {root}
862
+ {editable}
922
863
  />
923
864
  </div>
924
865
  </td>
@@ -934,8 +875,20 @@
934
875
  boundedheight={false}
935
876
  disable_click={true}
936
877
  {root}
937
- on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
878
+ on:load={({ detail }) =>
879
+ handle_file_upload(
880
+ detail.data,
881
+ col_count,
882
+ (head) => {
883
+ _headers = make_headers(head);
884
+ return _headers;
885
+ },
886
+ (vals) => {
887
+ values = vals;
888
+ }
889
+ )}
938
890
  bind:dragging
891
+ aria_label={i18n("dataframe.drop_to_upload")}
939
892
  >
940
893
  <VirtualTable
941
894
  bind:items={data}
@@ -963,6 +916,7 @@
963
916
  <div class="cell-wrap">
964
917
  <div class="header-content">
965
918
  <EditableCell
919
+ {max_chars}
966
920
  bind:value={_headers[i].value}
967
921
  bind:el={els[id].input}
968
922
  {latex_delimiters}
@@ -972,6 +926,7 @@
972
926
  on:dblclick={() => edit_header(i)}
973
927
  header
974
928
  {root}
929
+ {editable}
975
930
  />
976
931
  <button
977
932
  class:sorted={sort_by === i}
@@ -1000,7 +955,7 @@
1000
955
  class="cell-menu-button"
1001
956
  on:click={(event) => toggle_header_menu(event, i)}
1002
957
  >
1003
-
958
+ &#8942;
1004
959
  </button>
1005
960
  {/if}
1006
961
  </div>
@@ -1016,40 +971,24 @@
1016
971
  <td
1017
972
  tabindex="0"
1018
973
  on:touchstart={(event) => {
1019
- event.preventDefault();
1020
- event.stopPropagation();
1021
- clear_on_focus = false;
1022
- clicked_cell = { row: index, col: j };
1023
- selected = [index, j];
1024
- selected_header = false;
1025
- header_edit = false;
1026
- if (editable) {
1027
- editing = [index, j];
1028
- }
1029
- toggle_cell_button(index, j);
974
+ const touch = event.touches[0];
975
+ const mouseEvent = new MouseEvent("click", {
976
+ clientX: touch.clientX,
977
+ clientY: touch.clientY,
978
+ bubbles: true,
979
+ cancelable: true,
980
+ view: window
981
+ });
982
+ handle_cell_click(mouseEvent, index, j);
1030
983
  }}
1031
984
  on:mousedown={(event) => {
1032
985
  event.preventDefault();
1033
986
  event.stopPropagation();
1034
987
  }}
1035
- on:click={(event) => {
1036
- event.preventDefault();
1037
- event.stopPropagation();
1038
- clear_on_focus = false;
1039
- active_cell_menu = null;
1040
- active_header_menu = null;
1041
- clicked_cell = { row: index, col: j };
1042
- selected = [index, j];
1043
- selected_header = false;
1044
- header_edit = false;
1045
- if (editable) {
1046
- editing = [index, j];
1047
- }
1048
- toggle_cell_button(index, j);
1049
- }}
988
+ on:click={(event) => handle_cell_click(event, index, j)}
1050
989
  style:width="var(--cell-width-{j})"
1051
990
  style={styling?.[index]?.[j] || ""}
1052
- class:focus={dequal(selected, [index, j])}
991
+ class={is_cell_selected([index, j], selected_cells)}
1053
992
  class:menu-active={active_cell_menu &&
1054
993
  active_cell_menu.row === index &&
1055
994
  active_cell_menu.col === j}
@@ -1068,15 +1007,25 @@
1068
1007
  clear_on_focus = false;
1069
1008
  parent.focus();
1070
1009
  }}
1010
+ on:focus={() => {
1011
+ const row = index;
1012
+ const col = j;
1013
+ if (
1014
+ !selected_cells.some(([r, c]) => r === row && c === col)
1015
+ ) {
1016
+ selected_cells = [[row, col]];
1017
+ }
1018
+ }}
1071
1019
  {clear_on_focus}
1072
1020
  {root}
1021
+ {max_chars}
1073
1022
  />
1074
- {#if editable}
1023
+ {#if editable && should_show_cell_menu([index, j], selected_cells, editable)}
1075
1024
  <button
1076
1025
  class="cell-menu-button"
1077
1026
  on:click={(event) => toggle_cell_menu(event, index, j)}
1078
1027
  >
1079
-
1028
+ &#8942;
1080
1029
  </button>
1081
1030
  {/if}
1082
1031
  </div>
@@ -1128,21 +1077,10 @@
1128
1077
  {/if}
1129
1078
 
1130
1079
  <style>
1131
- .button-wrap:hover svg {
1132
- color: var(--color-accent);
1133
- }
1134
-
1135
- .button-wrap svg {
1136
- margin-right: var(--size-1);
1137
- margin-left: -5px;
1138
- }
1139
-
1140
- .label p {
1141
- position: relative;
1142
- z-index: var(--layer-4);
1143
- margin-bottom: var(--size-2);
1144
- color: var(--block-label-text-color);
1145
- font-size: var(--block-label-text-size);
1080
+ .table-container {
1081
+ display: flex;
1082
+ flex-direction: column;
1083
+ gap: var(--size-2);
1146
1084
  }
1147
1085
 
1148
1086
  .table-wrap {
@@ -1155,7 +1093,6 @@
1155
1093
 
1156
1094
  .table-wrap:focus-within {
1157
1095
  outline: none;
1158
- background-color: none;
1159
1096
  }
1160
1097
 
1161
1098
  .dragging {
@@ -1177,6 +1114,7 @@
1177
1114
  line-height: var(--line-md);
1178
1115
  font-family: var(--font-mono);
1179
1116
  border-spacing: 0;
1117
+ border-collapse: separate;
1180
1118
  }
1181
1119
 
1182
1120
  div:not(.no-wrap) td {
@@ -1231,6 +1169,12 @@
1231
1169
  th.focus,
1232
1170
  td.focus {
1233
1171
  --ring-color: var(--color-accent);
1172
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1173
+ z-index: var(--layer-1);
1174
+ }
1175
+
1176
+ th.focus {
1177
+ z-index: var(--layer-2);
1234
1178
  }
1235
1179
 
1236
1180
  tr:last-child td:first-child {
@@ -1259,7 +1203,6 @@
1259
1203
  cursor: pointer;
1260
1204
  padding: var(--size-2);
1261
1205
  color: var(--body-text-color-subdued);
1262
- line-height: var(--text-sm);
1263
1206
  }
1264
1207
 
1265
1208
  .sort-button:hover {
@@ -1280,11 +1223,11 @@
1280
1223
 
1281
1224
  .cell-wrap {
1282
1225
  display: flex;
1283
- align-items: center;
1226
+ align-items: flex-start;
1284
1227
  outline: none;
1285
- height: var(--size-full);
1286
1228
  min-height: var(--size-9);
1287
- overflow: hidden;
1229
+ position: relative;
1230
+ height: auto;
1288
1231
  }
1289
1232
 
1290
1233
  .header-content {
@@ -1293,12 +1236,9 @@
1293
1236
  overflow: hidden;
1294
1237
  flex-grow: 1;
1295
1238
  min-width: 0;
1296
- }
1297
-
1298
- .controls-wrap {
1299
- display: flex;
1300
- justify-content: flex-end;
1301
- padding-top: var(--size-2);
1239
+ white-space: normal;
1240
+ overflow-wrap: break-word;
1241
+ word-break: break-word;
1302
1242
  }
1303
1243
 
1304
1244
  .row_odd {
@@ -1309,10 +1249,6 @@
1309
1249
  background: var(--background-fill-primary);
1310
1250
  }
1311
1251
 
1312
- table {
1313
- border-collapse: separate;
1314
- }
1315
-
1316
1252
  .cell-menu-button {
1317
1253
  flex-shrink: 0;
1318
1254
  display: none;
@@ -1325,28 +1261,35 @@
1325
1261
  padding: 0;
1326
1262
  margin-right: var(--spacing-sm);
1327
1263
  z-index: var(--layer-1);
1264
+ position: absolute;
1265
+ right: var(--size-1);
1266
+ top: 50%;
1267
+ transform: translateY(-50%);
1328
1268
  }
1329
1269
 
1330
- .cell-menu-button:hover {
1331
- background-color: var(--color-bg-hover);
1270
+ .cell-selected .cell-menu-button {
1271
+ display: flex;
1272
+ align-items: center;
1273
+ justify-content: center;
1332
1274
  }
1333
1275
 
1334
- td.focus .cell-menu-button {
1276
+ .header-row {
1335
1277
  display: flex;
1278
+ justify-content: space-between;
1336
1279
  align-items: center;
1337
- justify-content: center;
1280
+ gap: var(--size-2);
1281
+ height: var(--size-6);
1282
+ min-height: var(--size-6);
1338
1283
  }
1339
1284
 
1340
- th .header-content {
1341
- white-space: normal;
1342
- overflow-wrap: break-word;
1343
- word-break: break-word;
1285
+ .label {
1286
+ flex: 1;
1344
1287
  }
1345
1288
 
1346
- .table-container {
1347
- display: flex;
1348
- flex-direction: column;
1349
- gap: var(--size-2);
1289
+ .label p {
1290
+ margin: 0;
1291
+ color: var(--block-label-text-color);
1292
+ font-size: var(--block-label-text-size);
1350
1293
  }
1351
1294
 
1352
1295
  .row-number,
@@ -1378,24 +1321,94 @@
1378
1321
  background: var(--table-odd-background-fill);
1379
1322
  }
1380
1323
 
1381
- .header-row {
1382
- display: flex;
1383
- justify-content: space-between;
1384
- align-items: center;
1385
- gap: var(--size-2);
1386
- height: var(--size-6);
1387
- min-height: var(--size-6);
1324
+ .cell-selected {
1325
+ --ring-color: var(--color-accent);
1326
+ box-shadow: inset 0 0 0 2px var(--ring-color);
1327
+ z-index: var(--layer-1);
1328
+ position: relative;
1388
1329
  }
1389
1330
 
1390
- .label {
1391
- flex: 1;
1331
+ .cell-selected.no-top {
1332
+ box-shadow:
1333
+ inset 2px 0 0 var(--ring-color),
1334
+ inset -2px 0 0 var(--ring-color),
1335
+ inset 0 -2px 0 var(--ring-color);
1392
1336
  }
1393
1337
 
1394
- .label p {
1395
- position: relative;
1396
- z-index: var(--layer-4);
1397
- margin: 0;
1398
- color: var(--block-label-text-color);
1399
- font-size: var(--block-label-text-size);
1338
+ .cell-selected.no-bottom {
1339
+ box-shadow:
1340
+ inset 2px 0 0 var(--ring-color),
1341
+ inset -2px 0 0 var(--ring-color),
1342
+ inset 0 2px 0 var(--ring-color);
1343
+ }
1344
+
1345
+ .cell-selected.no-left {
1346
+ box-shadow:
1347
+ inset 0 2px 0 var(--ring-color),
1348
+ inset -2px 0 0 var(--ring-color),
1349
+ inset 0 -2px 0 var(--ring-color);
1350
+ }
1351
+
1352
+ .cell-selected.no-right {
1353
+ box-shadow:
1354
+ inset 0 2px 0 var(--ring-color),
1355
+ inset 2px 0 0 var(--ring-color),
1356
+ inset 0 -2px 0 var(--ring-color);
1357
+ }
1358
+
1359
+ .cell-selected.no-top.no-left {
1360
+ box-shadow:
1361
+ inset -2px 0 0 var(--ring-color),
1362
+ inset 0 -2px 0 var(--ring-color);
1363
+ }
1364
+
1365
+ .cell-selected.no-top.no-right {
1366
+ box-shadow:
1367
+ inset 2px 0 0 var(--ring-color),
1368
+ inset 0 -2px 0 var(--ring-color);
1369
+ }
1370
+
1371
+ .cell-selected.no-bottom.no-left {
1372
+ box-shadow:
1373
+ inset -2px 0 0 var(--ring-color),
1374
+ inset 0 2px 0 var(--ring-color);
1375
+ }
1376
+
1377
+ .cell-selected.no-bottom.no-right {
1378
+ box-shadow:
1379
+ inset 2px 0 0 var(--ring-color),
1380
+ inset 0 2px 0 var(--ring-color);
1381
+ }
1382
+
1383
+ .cell-selected.no-top.no-bottom {
1384
+ box-shadow:
1385
+ inset 2px 0 0 var(--ring-color),
1386
+ inset -2px 0 0 var(--ring-color);
1387
+ }
1388
+
1389
+ .cell-selected.no-left.no-right {
1390
+ box-shadow:
1391
+ inset 0 2px 0 var(--ring-color),
1392
+ inset 0 -2px 0 var(--ring-color);
1393
+ }
1394
+
1395
+ .cell-selected.no-top.no-left.no-right {
1396
+ box-shadow: inset 0 -2px 0 var(--ring-color);
1397
+ }
1398
+
1399
+ .cell-selected.no-bottom.no-left.no-right {
1400
+ box-shadow: inset 0 2px 0 var(--ring-color);
1401
+ }
1402
+
1403
+ .cell-selected.no-left.no-top.no-bottom {
1404
+ box-shadow: inset -2px 0 0 var(--ring-color);
1405
+ }
1406
+
1407
+ .cell-selected.no-right.no-top.no-bottom {
1408
+ box-shadow: inset 2px 0 0 var(--ring-color);
1409
+ }
1410
+
1411
+ .cell-selected.no-top.no-bottom.no-left.no-right {
1412
+ box-shadow: none;
1400
1413
  }
1401
1414
  </style>