svelte-tably 1.1.2 → 1.2.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.
@@ -8,12 +8,8 @@
8
8
 
9
9
  -->
10
10
 
11
- <script lang="ts">
12
- import { type Snippet } from 'svelte'
13
- import { fly } from 'svelte/transition'
14
- import { sineInOut } from 'svelte/easing'
15
- import reorder, { type ItemState } from 'runic-reorder'
16
- import { Virtualization } from './virtualization.svelte.js'
11
+ <script module lang="ts">
12
+ import type { Snippet } from 'svelte'
17
13
  import {
18
14
  TableState,
19
15
  type HeaderSelectCtx,
@@ -21,36 +17,43 @@
21
17
  type RowSelectCtx,
22
18
  type TableProps
23
19
  } from './table-state.svelte.js'
24
- import Panel from '../panel/Panel.svelte'
25
20
  import Column from '../column/Column.svelte'
26
- import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
27
- import { conditional } from '../conditional.svelte.js'
28
- import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
21
+ import Panel from '../panel/Panel.svelte'
29
22
  import Expandable from '../expandable/Expandable.svelte'
30
- import { SizeTween } from '../size-tween.svelte.js'
31
- import { on } from 'svelte/events'
32
23
  import Row from '../row/Row.svelte'
33
- import type { CSVOptions } from './csv.js'
34
-
35
- type T = $$Generic<Record<PropertyKey, unknown>>
36
24
 
37
- type ConstructorReturnType<T extends new (...args: any[]) => any> =
38
- T extends new (...args: any[]) => infer K ? K : never
39
- type ConstructorParams<T extends new (...args: any[]) => any> =
40
- T extends new (...args: infer K) => any ? K : never
25
+ type ConstructorReturnType<C extends new (...args: any[]) => any> =
26
+ C extends new (...args: any[]) => infer K ? K : never
27
+ type ConstructorParams<C extends new (...args: any[]) => any> =
28
+ C extends new (...args: infer K) => any ? K : never
41
29
 
42
- type ContentCtx<T extends Record<PropertyKey, unknown>> = {
30
+ export type ContentCtx<Item = any> = {
43
31
  Column: {
44
- new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
45
- <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
32
+ new <V>(...args: ConstructorParams<typeof Column<Item, V>>): ConstructorReturnType<typeof Column<Item, V>>
33
+ <V>(...args: Parameters<typeof Column<Item, V>>): ReturnType<typeof Column<Item, V>>
46
34
  }
47
- Panel: typeof Panel<T>
48
- Expandable: typeof Expandable<T>
49
- Row: typeof Row<T>
50
- readonly table: TableState<T>
35
+ Panel: typeof Panel<Item>
36
+ Expandable: typeof Expandable<Item>
37
+ Row: typeof Row<Item>
38
+ readonly table: TableState<Item>
51
39
  }
52
40
 
53
- type ContentSnippet = Snippet<[context: ContentCtx<T>]>
41
+ export type ContentSnippet<Item = any> = Snippet<[context: ContentCtx<Item>]>
42
+ </script>
43
+
44
+ <script lang="ts">
45
+ import { fly } from 'svelte/transition'
46
+ import { sineInOut } from 'svelte/easing'
47
+ import reorder, { type ItemState } from 'runic-reorder'
48
+ import { Virtualization } from './virtualization.svelte.js'
49
+ import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
50
+ import { conditional } from '../conditional.svelte.js'
51
+ import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
52
+ import { SizeTween } from '../size-tween.svelte.js'
53
+ import { on } from 'svelte/events'
54
+ import type { CSVOptions } from './csv.js'
55
+
56
+ type T = $$Generic
54
57
 
55
58
  let {
56
59
  content,
@@ -59,7 +62,7 @@
59
62
  data: _data = $bindable([]),
60
63
  table: _table = $bindable(),
61
64
  ...restProps
62
- }: TableProps<T> & { content?: ContentSnippet } = $props()
65
+ }: TableProps<T> & { content?: ContentSnippet<T> } = $props()
63
66
 
64
67
  const properties = fromProps(restProps, {
65
68
  selected: [() => _selected, (v) => (_selected = v)],
@@ -69,14 +72,39 @@
69
72
 
70
73
  const mount = mounted()
71
74
 
75
+
76
+ const getRowLabel = (item: T, index: number) => {
77
+ const labelColumn = columns.find((c) => c.id !== '__fixed')
78
+ const raw = labelColumn?.options.value?.(item)
79
+
80
+ if (raw === null || raw === undefined) return `Row ${index + 1}`
81
+ if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
82
+ const text = String(raw).trim()
83
+ return text ? text : `Row ${index + 1}`
84
+ }
85
+
86
+ return `Row ${index + 1}`
87
+ }
88
+
72
89
  const reorderArea = reorder(rowSnippet)
73
90
 
74
- const elements = $state({}) as Record<
75
- 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
76
- HTMLElement
77
- >
91
+ const elements = $state({}) as Record<'headers' | 'statusbar', HTMLElement>
78
92
 
79
93
  const table = new TableState<T>(properties) as TableState<T>
94
+ const uid = table.cssId
95
+ let expandIdCounter = 0
96
+ const expandIds = new WeakMap<object, string>()
97
+ const getExpandId = (item: T) => {
98
+ if (item && typeof item === 'object') {
99
+ let id = expandIds.get(item)
100
+ if (!id) {
101
+ id = `${uid}-expand-${++expandIdCounter}`
102
+ expandIds.set(item, id)
103
+ }
104
+ return id
105
+ }
106
+ return `${uid}-expand-${String(item)}`
107
+ }
80
108
 
81
109
  const virtualization = new Virtualization(table)
82
110
 
@@ -86,13 +114,38 @@
86
114
  let hoveredColumn: ColumnState | null = $state(null)
87
115
 
88
116
  /** Order of columns */
89
- const fixed = $derived(table.positions.fixed)
90
- const hidden = $derived(table.positions.hidden)
91
- const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
117
+ const isColumn = (value: unknown): value is ColumnState =>
118
+ value instanceof ColumnState
119
+ const fixed = $derived(table.positions.fixed.filter(isColumn))
120
+ const hidden = $derived(table.positions.hidden.filter(isColumn))
121
+ const notHidden = (column: ColumnState | undefined) =>
122
+ !!column && !table.positions.hidden.includes(column)
92
123
  const sticky = $derived(table.positions.sticky.filter(notHidden))
93
124
  const scrolled = $derived(table.positions.scroll.filter(notHidden))
94
125
  const columns = $derived([...fixed, ...sticky, ...scrolled])
95
126
 
127
+ const autoSchema = $derived.by(() => {
128
+ const rows = table.dataState.origin as any[]
129
+ const keys = [] as string[]
130
+ const seen = new Set<string>()
131
+ const sample = {} as Record<string, unknown>
132
+
133
+ for (const row of rows.slice(0, 50)) {
134
+ if (!row || typeof row !== 'object') continue
135
+ for (const key of Object.keys(row)) {
136
+ if (!seen.has(key)) {
137
+ seen.add(key)
138
+ keys.push(key)
139
+ }
140
+ if (sample[key] === undefined && row[key] !== undefined) {
141
+ sample[key] = row[key]
142
+ }
143
+ }
144
+ }
145
+
146
+ return { keys, sample }
147
+ })
148
+
96
149
  const getWidth = (key: string, def: number = 150) =>
97
150
  table.columnWidths[key] ??= table.columns[key]?.defaults.width ?? def
98
151
 
@@ -104,7 +157,7 @@
104
157
  return
105
158
  }
106
159
 
107
- const context = table.row?.snippets.context ? table.row?.options.context.width : ''
160
+ const context = table.row?.snippets.context ? ` ${table.row?.options.context.width}` : ''
108
161
 
109
162
  const templateColumns =
110
163
  columns
@@ -115,7 +168,7 @@
115
168
  })
116
169
  .join(' ') + context
117
170
 
118
- const theadTempla3teColumns = `
171
+ const theadTemplateColumns = `
119
172
  [data-svelte-tably="${table.cssId}"] > thead > tr,
120
173
  [data-svelte-tably="${table.cssId}"] > tfoot > tr {
121
174
  grid-template-columns: ${templateColumns};
@@ -124,6 +177,7 @@
124
177
 
125
178
  const tbodyTemplateColumns = `
126
179
  [data-area-class='${table.cssId}'] tr.row,
180
+ [data-area-class='${table.cssId}'] tr.filler,
127
181
  [data-svelte-tably="${table.cssId}"] > tbody::after {
128
182
  grid-template-columns: ${templateColumns};
129
183
  }
@@ -154,7 +208,7 @@
154
208
  )
155
209
  .join('')
156
210
 
157
- style = theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
211
+ style = theadTemplateColumns + tbodyTemplateColumns + stickyLeft + columnStyling
158
212
  })
159
213
 
160
214
  function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
@@ -188,16 +242,32 @@
188
242
  }
189
243
 
190
244
  let tbody = $state({
191
- width: 0
245
+ scrollbar: 0
192
246
  })
193
- async function onscroll() {
194
- const target = virtualization.viewport.element!
195
- if (target.scrollTop !== virtualization.scrollTop) {
196
- virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
247
+
248
+ function observeScrollbar(node: HTMLElement) {
249
+ if (typeof ResizeObserver === 'undefined') return
250
+
251
+ const update = () => {
252
+ // Reserve the same gutter in header/footer as the scrollable body
253
+ tbody.scrollbar = Math.max(0, node.offsetWidth - node.clientWidth)
197
254
  }
198
255
 
199
- if (elements.selects) {
200
- elements.selects.scrollTop = target?.scrollTop
256
+ update()
257
+ const observer = new ResizeObserver(update)
258
+ observer.observe(node)
259
+
260
+ return {
261
+ destroy() {
262
+ observer.disconnect()
263
+ }
264
+ }
265
+ }
266
+ function onscroll() {
267
+ const target = virtualization.viewport.element
268
+ if (!target) return
269
+ if (target.scrollTop !== virtualization.scrollTop) {
270
+ virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
201
271
  }
202
272
 
203
273
  if (elements.headers) {
@@ -415,7 +485,11 @@
415
485
  class:fixed={true}
416
486
  use:addRowColumnEvents={[where, column, () => args[1]]}
417
487
  data-column={column.id}
418
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
488
+ class:pad={
489
+ (where === 'header' && column.options.padHeader) ||
490
+ (where === 'row' && column.options.padRow) ||
491
+ (where === 'statusbar' && column.options.padStatusbar)
492
+ }
419
493
  class:header={isHeader}
420
494
  class:sortable
421
495
  use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
@@ -443,7 +517,11 @@
443
517
  use:addRowColumnEvents={[where, column, () => args[1]]}
444
518
  use:observeColumnWidth={isHeader}
445
519
  data-column={column.id}
446
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
520
+ class:pad={
521
+ (where === 'header' && column.options.padHeader) ||
522
+ (where === 'row' && column.options.padRow) ||
523
+ (where === 'statusbar' && column.options.padStatusbar)
524
+ }
447
525
  class:header={isHeader}
448
526
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
449
527
  class:border={i == sticky.length - 1}
@@ -470,7 +548,11 @@
470
548
  class={column.options.class ?? ''}
471
549
  class:column={true}
472
550
  data-column={column.id}
473
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
551
+ class:pad={
552
+ (where === 'header' && column.options.padHeader) ||
553
+ (where === 'row' && column.options.padRow) ||
554
+ (where === 'statusbar' && column.options.padStatusbar)
555
+ }
474
556
  use:addRowColumnEvents={[where, column, () => args[1]]}
475
557
  use:observeColumnWidth={isHeader}
476
558
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
@@ -490,7 +572,7 @@
490
572
  {/each}
491
573
  {/snippet}
492
574
 
493
- {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
575
+ {#snippet defaultRow(item: any, ctx: RowColumnCtx<any, any>)}
494
576
  {ctx.value}
495
577
  {/snippet}
496
578
 
@@ -566,30 +648,46 @@
566
648
  'row'
567
649
  )}
568
650
  {#if table.row?.snippets.context}
569
- {#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
570
- <td
571
- class="context-col"
572
- class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
573
- class:hidden={table.row?.options.context.hover &&
574
- table.row?.snippets.contextHeader &&
575
- hoveredRow !== item}
576
- >
577
- {@render table.row?.snippets.context?.(item, ctx)}
578
- </td>
579
- {/if}
651
+ <td
652
+ class="context-col"
653
+ class:hidden={table.row?.options.context.hover && hoveredRow !== item}
654
+ >
655
+ {@render table.row?.snippets.context?.(item, ctx)}
656
+ </td>
580
657
  {/if}
581
658
  </tr>
582
659
 
583
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
584
- min: 1,
660
+ {@const expandableTween = new SizeTween(() => !!table.expandable && expandedRow.includes(item), {
661
+ min: 0,
585
662
  duration: table.expandable?.options.slide.duration,
586
663
  easing: table.expandable?.options.slide.easing
587
664
  })}
588
- {#if expandableTween.current > 0}
589
- <tr class="expandable" style="height: {expandableTween.current}px">
590
- <td colspan={columns.length} style="height: {expandableTween.current}px">
591
- <div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
592
- {@render table.expandable!.snippets.content?.(item, ctx)}
665
+ {@const expanded = !!table.expandable && expandedRow.includes(item)}
666
+ {#if table.expandable && (expanded || expandableTween.current > 0 || expandableTween.transitioning)}
667
+ {@const expandId = getExpandId(item)}
668
+ {@const expandLabelId = `${expandId}-label`}
669
+ <tr class="expandable">
670
+ <td
671
+ colspan={columns.length + (table.row?.snippets.context ? 1 : 0)}
672
+ style="padding: 0"
673
+ >
674
+ <div
675
+ class="expandable-clip"
676
+ style="height: {Math.round(expandableTween.current)}px"
677
+ id={expandId}
678
+ role="region"
679
+ aria-labelledby={expandLabelId}
680
+ aria-hidden={!expanded}
681
+ >
682
+ <span class="sr-only" id={expandLabelId}>
683
+ Expanded content for {getRowLabel(item, index)}
684
+ </span>
685
+ <div
686
+ class="expandable-content"
687
+ bind:offsetHeight={expandableTween.size}
688
+ >
689
+ {@render table.expandable?.snippets.content?.(item, ctx)}
690
+ </div>
593
691
  </div>
594
692
  </td>
595
693
  </tr>
@@ -600,7 +698,7 @@
600
698
  id={table.id}
601
699
  data-svelte-tably={table.cssId}
602
700
  class="table svelte-tably"
603
- style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
701
+ style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px; --scrollbar: {tbody.scrollbar}px;"
604
702
  aria-rowcount={table.data.length}
605
703
  >
606
704
  {#if columns.some((v) => v.snippets.header)}
@@ -620,23 +718,28 @@
620
718
  ],
621
719
  'header'
622
720
  )}
623
- {#if table.row?.snippets.contextHeader}
624
- <th class="context-col">
625
- {@render table.row?.snippets.contextHeader()}
721
+ {#if table.row?.snippets.context}
722
+ <th
723
+ class="context-col"
724
+ aria-hidden={table.row?.snippets.contextHeader ? undefined : true}
725
+ role={table.row?.snippets.contextHeader ? undefined : 'presentation'}
726
+ >
727
+ {#if table.row?.snippets.contextHeader}
728
+ {@render table.row?.snippets.contextHeader()}
729
+ {/if}
626
730
  </th>
627
731
  {/if}
628
732
  </tr>
629
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
630
733
  </thead>
631
734
  {/if}
632
735
 
633
736
  <tbody
634
737
  class="content"
635
738
  use:reorderArea={{ axis: 'y', class: table.cssId }}
739
+ use:observeScrollbar
636
740
  bind:this={virtualization.viewport.element}
637
741
  onscrollcapture={onscroll}
638
742
  bind:clientHeight={virtualization.viewport.height}
639
- bind:clientWidth={tbody.width}
640
743
  >
641
744
  {#if table.options.reorderable}
642
745
  {@render reorderArea({
@@ -655,6 +758,39 @@
655
758
  {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
656
759
  {/each}
657
760
  {/if}
761
+
762
+ {#if columns.length > 0 && virtualization.virtualTop === 0 && virtualization.virtualBottom === 0}
763
+ <tr class="filler" aria-hidden="true">
764
+ {#each fixed as column (column)}
765
+ {#if !hidden.includes(column)}
766
+ <td
767
+ class={`column sticky fixed ${column.options.class ?? ''}`}
768
+ data-column={column.id}
769
+ ></td>
770
+ {/if}
771
+ {/each}
772
+ {#each sticky as column, i (column)}
773
+ {#if !hidden.includes(column)}
774
+ <td
775
+ class={`column sticky ${column.options.class ?? ''}`}
776
+ class:border={i == sticky.length - 1}
777
+ data-column={column.id}
778
+ ></td>
779
+ {/if}
780
+ {/each}
781
+ {#each scrolled as column (column)}
782
+ {#if !hidden.includes(column)}
783
+ <td
784
+ class={`column ${column.options.class ?? ''}`}
785
+ data-column={column.id}
786
+ ></td>
787
+ {/if}
788
+ {/each}
789
+ {#if table.row?.snippets.context}
790
+ <td class="context-col" aria-hidden="true"></td>
791
+ {/if}
792
+ </tr>
793
+ {/if}
658
794
  </tbody>
659
795
 
660
796
  {#if columns.some((v) => v.snippets.statusbar)}
@@ -671,8 +807,10 @@
671
807
  ],
672
808
  'statusbar'
673
809
  )}
810
+ {#if table.row?.snippets.context}
811
+ <td class="context-col" aria-hidden="true"></td>
812
+ {/if}
674
813
  </tr>
675
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
676
814
  </tfoot>
677
815
  {/if}
678
816
 
@@ -708,11 +846,11 @@
708
846
  </caption>
709
847
  </table>
710
848
 
711
- {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
849
+ {#snippet headerSelected(ctx: HeaderSelectCtx<any>)}
712
850
  <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
713
851
  {/snippet}
714
852
 
715
- {#snippet rowSelected(ctx: RowSelectCtx<T>)}
853
+ {#snippet rowSelected(ctx: RowSelectCtx<any>)}
716
854
  <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
717
855
  {/snippet}
718
856
 
@@ -798,9 +936,20 @@
798
936
  })}
799
937
  {/if}
800
938
  {#if expandable && expandable?.options.chevron !== 'never'}
801
- <button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
802
- {#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
803
- {@render chevronSnippet(row.expanded ? 180 : 90)}
939
+ {@const expandId = getExpandId(item)}
940
+ {@const expanded = row.expanded}
941
+ {@const label = expanded ? 'Collapse row' : 'Expand row'}
942
+ <button
943
+ class="expand-row"
944
+ tabindex="-1"
945
+ type="button"
946
+ aria-label={label}
947
+ aria-expanded={expanded}
948
+ aria-controls={expandId}
949
+ onclick={() => (row.expanded = !row.expanded)}
950
+ >
951
+ {#if expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
952
+ {@render chevronSnippet(expanded ? 180 : 90)}
804
953
  {/if}
805
954
  </button>
806
955
  {/if}
@@ -811,12 +960,12 @@
811
960
  {/if}
812
961
 
813
962
  {#if table.options.auto}
814
- {#each Object.keys(table.data[0] || {}) as key}
963
+ {#each autoSchema.keys as key}
815
964
  <Column
816
965
  id={key}
817
- value={(r) => r[key]}
966
+ value={(r) => (r as any)?.[key]}
818
967
  header={capitalize(segmentize(key))}
819
- sort={typeof table.data[0]?.[key] === 'number' ?
968
+ sort={typeof autoSchema.sample?.[key] === 'number' ?
820
969
  (a, b) => a - b
821
970
  : (a, b) => String(a).localeCompare(String(b))}
822
971
  />
@@ -850,14 +999,9 @@
850
999
  height: 100%;
851
1000
  z-index: 3;
852
1001
  padding: 0;
853
-
854
- &.hover {
855
- position: absolute;
856
- }
857
1002
  &.hidden {
858
1003
  pointer-events: none;
859
1004
  user-select: none;
860
- border-left: none;
861
1005
  background: none;
862
1006
  > :global(*) {
863
1007
  opacity: 0;
@@ -865,6 +1009,30 @@
865
1009
  }
866
1010
  }
867
1011
 
1012
+ .table::before {
1013
+ content: '';
1014
+ grid-area: headers;
1015
+ justify-self: end;
1016
+ align-self: stretch;
1017
+ width: var(--scrollbar, 0px);
1018
+ background-color: var(--tably-bg);
1019
+ pointer-events: none;
1020
+ position: relative;
1021
+ z-index: 4;
1022
+ }
1023
+
1024
+ .table::after {
1025
+ content: '';
1026
+ grid-area: statusbar;
1027
+ justify-self: end;
1028
+ align-self: stretch;
1029
+ width: var(--scrollbar, 0px);
1030
+ background-color: var(--tably-statusbar);
1031
+ pointer-events: none;
1032
+ position: relative;
1033
+ z-index: 4;
1034
+ }
1035
+
868
1036
  :global(:root) {
869
1037
  --tably-color: hsl(0, 0%, 0%);
870
1038
  --tably-bg: hsl(0, 0%, 100%);
@@ -881,24 +1049,32 @@
881
1049
 
882
1050
  .svelte-tably {
883
1051
  position: relative;
884
- overflow: visible;
1052
+ overflow: hidden;
1053
+ border-collapse: collapse;
1054
+ border-spacing: 0;
885
1055
  }
886
1056
 
887
1057
  .expandable {
888
- position: relative;
889
-
890
1058
  & > td {
891
- position: sticky;
892
- left: 1px;
893
- > div {
894
- position: absolute;
895
- overflow: auto;
896
- top: -1.5px;
897
- left: 0;
898
- }
1059
+ padding: 0;
1060
+ border: none;
899
1061
  }
900
1062
  }
901
1063
 
1064
+ .expandable-clip {
1065
+ overflow: hidden;
1066
+ width: 100%;
1067
+ background-color: var(--tably-bg);
1068
+ box-shadow: inset 0 -1px 0 var(--tably-border-grid);
1069
+ }
1070
+
1071
+ .expandable-content {
1072
+ overflow: auto;
1073
+ width: 100%;
1074
+ background-color: var(--tably-bg);
1075
+ box-sizing: border-box;
1076
+ }
1077
+
902
1078
  .expand-row {
903
1079
  display: flex;
904
1080
  justify-content: center;
@@ -965,20 +1141,16 @@
965
1141
  }
966
1142
 
967
1143
  tbody::before,
968
- tbody::after,
969
- selects::before,
970
- selects::after {
1144
+ tbody::after {
971
1145
  content: '';
972
- display: grid;
973
- min-height: 100%;
1146
+ display: block;
1147
+ flex: 0 0 auto;
974
1148
  }
975
1149
 
976
- tbody::before,
977
- selects::before {
1150
+ tbody::before {
978
1151
  height: var(--t);
979
1152
  }
980
- tbody::after,
981
- selects::after {
1153
+ tbody::after {
982
1154
  height: var(--b);
983
1155
  }
984
1156
 
@@ -1046,7 +1218,10 @@
1046
1218
 
1047
1219
  .table {
1048
1220
  display: grid;
1049
- height: auto;
1221
+ width: 100%;
1222
+ min-width: 0;
1223
+ min-height: 0;
1224
+ height: 100%;
1050
1225
  max-height: 100%;
1051
1226
  position: relative;
1052
1227
 
@@ -1058,8 +1233,8 @@
1058
1233
  'rows panel'
1059
1234
  'statusbar panel';
1060
1235
 
1061
- grid-template-columns: auto min-content;
1062
- grid-template-rows: auto 1fr auto;
1236
+ grid-template-columns: 1fr min-content;
1237
+ grid-template-rows: auto minmax(0, 1fr) auto;
1063
1238
 
1064
1239
  border: 1px solid var(--tably-border);
1065
1240
  border-radius: var(--tably-radius);
@@ -1070,6 +1245,8 @@
1070
1245
  grid-area: headers;
1071
1246
  z-index: 2;
1072
1247
  overflow: hidden;
1248
+ min-width: 0;
1249
+ padding-right: var(--scrollbar, 0px);
1073
1250
  }
1074
1251
 
1075
1252
  .headers > tr > .column {
@@ -1082,13 +1259,30 @@
1082
1259
  border-left: 1px solid var(--tably-border-grid);
1083
1260
  }
1084
1261
 
1262
+ .headers > tr > .context-col {
1263
+ background-color: var(--tably-bg);
1264
+ }
1265
+
1085
1266
  .content {
1086
- display: grid;
1087
- grid-auto-rows: max-content;
1267
+ display: flex;
1268
+ flex-direction: column;
1269
+ min-width: 0;
1270
+ min-height: 0;
1088
1271
 
1089
1272
  grid-area: rows;
1090
1273
  scrollbar-width: thin;
1091
- overflow: auto;
1274
+ overflow-x: auto;
1275
+ overflow-y: scroll;
1276
+ }
1277
+
1278
+ .content > tr.row,
1279
+ .content > tr.expandable {
1280
+ flex: 0 0 auto;
1281
+ }
1282
+
1283
+ .content > tr.filler {
1284
+ flex: 1 0 0px;
1285
+ min-height: 0;
1092
1286
  }
1093
1287
 
1094
1288
  .statusbar {
@@ -1096,19 +1290,30 @@
1096
1290
  grid-area: statusbar;
1097
1291
  overflow: hidden;
1098
1292
  background-color: var(--tably-statusbar);
1293
+ min-width: 0;
1294
+ padding-right: var(--scrollbar, 0px);
1099
1295
  }
1100
1296
 
1101
1297
  .statusbar > tr > .column {
1102
1298
  border-top: 1px solid var(--tably-border);
1103
1299
  padding: calc(var(--tably-padding-y) / 2) 0;
1104
1300
  }
1301
+ .statusbar > tr > .context-col {
1302
+ border-top: 1px solid var(--tably-border);
1303
+ border-left: 1px solid var(--tably-border-grid);
1304
+ }
1305
+
1306
+ .statusbar > tr > .context-col {
1307
+ background-color: var(--tably-statusbar);
1308
+ }
1105
1309
 
1106
1310
  .headers > tr,
1107
1311
  .row,
1312
+ .expandable,
1313
+ .filler,
1108
1314
  .statusbar > tr {
1109
1315
  display: grid;
1110
1316
  width: 100%;
1111
- height: 100%;
1112
1317
  min-width: max-content;
1113
1318
 
1114
1319
  & > .column {
@@ -1145,11 +1350,30 @@
1145
1350
  border-top: 2px solid var(--tably-border-grid);
1146
1351
  }
1147
1352
 
1148
- .row > * {
1353
+ .row > *,
1354
+ .filler > * {
1149
1355
  border-left: 1px solid var(--tably-border-grid);
1150
1356
  border-bottom: 1px solid var(--tably-border-grid);
1151
1357
  }
1152
1358
 
1359
+ .filler {
1360
+ pointer-events: none;
1361
+ user-select: none;
1362
+ background: none;
1363
+ }
1364
+
1365
+ .sr-only {
1366
+ position: absolute;
1367
+ width: 1px;
1368
+ height: 1px;
1369
+ padding: 0;
1370
+ margin: -1px;
1371
+ overflow: hidden;
1372
+ clip: rect(0, 0, 0, 0);
1373
+ white-space: nowrap;
1374
+ border: 0;
1375
+ }
1376
+
1153
1377
  .panel {
1154
1378
  position: relative;
1155
1379
  grid-area: panel;