svelte-tably 1.1.2 → 1.3.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
24
 
35
- type T = $$Generic<Record<PropertyKey, unknown>>
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
36
29
 
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
41
-
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,16 +114,100 @@
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
 
152
+ const measureContextCellWidth = (cell: HTMLElement | null) => {
153
+ if (!cell) return 0
154
+ const inner = cell.querySelector(':scope > .context-inner') as HTMLElement | null
155
+ const content = inner?.firstElementChild as HTMLElement | null
156
+ const candidates = [cell, inner, content].filter(Boolean) as HTMLElement[]
157
+ let width = 0
158
+ for (const el of candidates) {
159
+ width = Math.max(
160
+ width,
161
+ Math.ceil(el.getBoundingClientRect().width),
162
+ Math.ceil(el.scrollWidth)
163
+ )
164
+ }
165
+ return width
166
+ }
167
+
168
+ let contextWidth = $state(0)
169
+ let contextWidthRaf = 0
170
+ $effect(() => {
171
+ if (!mount.isMounted) {
172
+ contextWidth = 0
173
+ if (contextWidthRaf) cancelAnimationFrame(contextWidthRaf)
174
+ return
175
+ }
176
+ if (!table.row?.snippets.context) {
177
+ contextWidth = 0
178
+ if (contextWidthRaf) cancelAnimationFrame(contextWidthRaf)
179
+ return
180
+ }
181
+ if (!table.row?.options.context.alignHeaderToRows) {
182
+ contextWidth = 0
183
+ if (contextWidthRaf) cancelAnimationFrame(contextWidthRaf)
184
+ return
185
+ }
186
+
187
+ virtualization.topIndex
188
+ if (contextWidthRaf) cancelAnimationFrame(contextWidthRaf)
189
+ contextWidthRaf = requestAnimationFrame(() => {
190
+ const headerCell = elements.headers?.querySelector(
191
+ '[data-tably-context-measure="header"]'
192
+ ) as HTMLElement | null
193
+ const rowCell = virtualization.viewport.element?.querySelector(
194
+ '[data-tably-context-measure="row"]'
195
+ ) as HTMLElement | null
196
+
197
+ const width = Math.max(
198
+ measureContextCellWidth(headerCell),
199
+ measureContextCellWidth(rowCell)
200
+ )
201
+ if (width > 0 && width !== contextWidth) {
202
+ contextWidth = width
203
+ }
204
+ })
205
+
206
+ return () => {
207
+ if (contextWidthRaf) cancelAnimationFrame(contextWidthRaf)
208
+ }
209
+ })
210
+
99
211
  /** grid-template-columns for widths */
100
212
  let style = $state('')
101
213
  $effect(() => {
@@ -104,7 +216,7 @@
104
216
  return
105
217
  }
106
218
 
107
- const context = table.row?.snippets.context ? table.row?.options.context.width : ''
219
+ const context = table.row?.snippets.context ? ' var(--tably-context-width)' : ''
108
220
 
109
221
  const templateColumns =
110
222
  columns
@@ -115,7 +227,7 @@
115
227
  })
116
228
  .join(' ') + context
117
229
 
118
- const theadTempla3teColumns = `
230
+ const theadTemplateColumns = `
119
231
  [data-svelte-tably="${table.cssId}"] > thead > tr,
120
232
  [data-svelte-tably="${table.cssId}"] > tfoot > tr {
121
233
  grid-template-columns: ${templateColumns};
@@ -124,6 +236,8 @@
124
236
 
125
237
  const tbodyTemplateColumns = `
126
238
  [data-area-class='${table.cssId}'] tr.row,
239
+ [data-area-class='${table.cssId}'] tr.expandable,
240
+ [data-area-class='${table.cssId}'] tr.filler,
127
241
  [data-svelte-tably="${table.cssId}"] > tbody::after {
128
242
  grid-template-columns: ${templateColumns};
129
243
  }
@@ -154,7 +268,7 @@
154
268
  )
155
269
  .join('')
156
270
 
157
- style = theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
271
+ style = theadTemplateColumns + tbodyTemplateColumns + stickyLeft + columnStyling
158
272
  })
159
273
 
160
274
  function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
@@ -188,16 +302,34 @@
188
302
  }
189
303
 
190
304
  let tbody = $state({
191
- width: 0
305
+ scrollbar: 0,
306
+ viewportWidth: 0
192
307
  })
193
- async function onscroll() {
194
- const target = virtualization.viewport.element!
195
- if (target.scrollTop !== virtualization.scrollTop) {
196
- virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
308
+
309
+ function observeScrollbar(node: HTMLElement) {
310
+ if (typeof ResizeObserver === 'undefined') return
311
+
312
+ const update = () => {
313
+ // Reserve the same gutter in header/footer as the scrollable body
314
+ tbody.scrollbar = Math.max(0, node.offsetWidth - node.clientWidth)
315
+ tbody.viewportWidth = node.clientWidth
197
316
  }
198
317
 
199
- if (elements.selects) {
200
- elements.selects.scrollTop = target?.scrollTop
318
+ update()
319
+ const observer = new ResizeObserver(update)
320
+ observer.observe(node)
321
+
322
+ return {
323
+ destroy() {
324
+ observer.disconnect()
325
+ }
326
+ }
327
+ }
328
+ function onscroll() {
329
+ const target = virtualization.viewport.element
330
+ if (!target) return
331
+ if (target.scrollTop !== virtualization.scrollTop) {
332
+ virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
201
333
  }
202
334
 
203
335
  if (elements.headers) {
@@ -415,7 +547,11 @@
415
547
  class:fixed={true}
416
548
  use:addRowColumnEvents={[where, column, () => args[1]]}
417
549
  data-column={column.id}
418
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
550
+ class:pad={
551
+ (where === 'header' && column.options.padHeader) ||
552
+ (where === 'row' && column.options.padRow) ||
553
+ (where === 'statusbar' && column.options.padStatusbar)
554
+ }
419
555
  class:header={isHeader}
420
556
  class:sortable
421
557
  use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
@@ -443,7 +579,11 @@
443
579
  use:addRowColumnEvents={[where, column, () => args[1]]}
444
580
  use:observeColumnWidth={isHeader}
445
581
  data-column={column.id}
446
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
582
+ class:pad={
583
+ (where === 'header' && column.options.padHeader) ||
584
+ (where === 'row' && column.options.padRow) ||
585
+ (where === 'statusbar' && column.options.padStatusbar)
586
+ }
447
587
  class:header={isHeader}
448
588
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
449
589
  class:border={i == sticky.length - 1}
@@ -470,7 +610,11 @@
470
610
  class={column.options.class ?? ''}
471
611
  class:column={true}
472
612
  data-column={column.id}
473
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
613
+ class:pad={
614
+ (where === 'header' && column.options.padHeader) ||
615
+ (where === 'row' && column.options.padRow) ||
616
+ (where === 'statusbar' && column.options.padStatusbar)
617
+ }
474
618
  use:addRowColumnEvents={[where, column, () => args[1]]}
475
619
  use:observeColumnWidth={isHeader}
476
620
  class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
@@ -490,7 +634,7 @@
490
634
  {/each}
491
635
  {/snippet}
492
636
 
493
- {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
637
+ {#snippet defaultRow(item: any, ctx: RowColumnCtx<any, any>)}
494
638
  {ctx.value}
495
639
  {/snippet}
496
640
 
@@ -508,9 +652,16 @@
508
652
  return table.selected?.includes(item)
509
653
  },
510
654
  set selected(value) {
511
- value ?
512
- table.selected!.push(item)
513
- : table.selected!.splice(table.selected!.indexOf(item), 1)
655
+ const current = table.selected
656
+ if (value) {
657
+ if (!current.includes(item)) {
658
+ table.selected = [...current, item]
659
+ }
660
+ return
661
+ }
662
+ if (current.includes(item)) {
663
+ table.selected = current.filter((v) => v !== item)
664
+ }
514
665
  },
515
666
  get itemState() {
516
667
  return itemState
@@ -537,10 +688,8 @@
537
688
  use:addRowEvents={ctx}
538
689
  onclick={(e) => {
539
690
  if (table.expandable?.options.click === true) {
540
- let target = e.target as HTMLElement
541
- if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
542
- return
543
- }
691
+ const target = e.target
692
+ if (target instanceof Element && target.closest('input, textarea, button, a')) return
544
693
  ctx.expanded = !ctx.expanded
545
694
  }
546
695
  }}
@@ -566,30 +715,57 @@
566
715
  'row'
567
716
  )}
568
717
  {#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
- >
718
+ <td
719
+ class="context-col"
720
+ class:hidden={table.row?.options.context.hover && hoveredRow !== item}
721
+ data-tably-context-measure={
722
+ table.row?.options.context.alignHeaderToRows &&
723
+ index === virtualization.topIndex ?
724
+ 'row'
725
+ : undefined
726
+ }
727
+ >
728
+ <div class="context-inner">
577
729
  {@render table.row?.snippets.context?.(item, ctx)}
578
- </td>
579
- {/if}
730
+ </div>
731
+ </td>
580
732
  {/if}
581
733
  </tr>
582
734
 
583
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
584
- min: 1,
735
+ {@const expandableTween = new SizeTween(() => !!table.expandable && expandedRow.includes(item), {
736
+ min: 0,
585
737
  duration: table.expandable?.options.slide.duration,
586
738
  easing: table.expandable?.options.slide.easing
587
739
  })}
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)}
740
+ {@const expanded = !!table.expandable && expandedRow.includes(item)}
741
+ {#if table.expandable && (expanded || expandableTween.current > 0 || expandableTween.transitioning)}
742
+ {@const expandId = getExpandId(item)}
743
+ {@const expandLabelId = `${expandId}-label`}
744
+ <tr class="expandable">
745
+ <td
746
+ class="expandable-cell"
747
+ colspan={columns.length + (table.row?.snippets.context ? 1 : 0)}
748
+ style="padding: 0"
749
+ >
750
+ <div class="expandable-sticky">
751
+ <div
752
+ class="expandable-clip"
753
+ style="height: {Math.round(expandableTween.current)}px"
754
+ id={expandId}
755
+ role="region"
756
+ aria-labelledby={expandLabelId}
757
+ aria-hidden={!expanded}
758
+ >
759
+ <span class="sr-only" id={expandLabelId}>
760
+ Expanded content for {getRowLabel(item, index)}
761
+ </span>
762
+ <div
763
+ class="expandable-content"
764
+ bind:offsetHeight={expandableTween.size}
765
+ >
766
+ {@render table.expandable?.snippets.content?.(item, ctx)}
767
+ </div>
768
+ </div>
593
769
  </div>
594
770
  </td>
595
771
  </tr>
@@ -600,7 +776,7 @@
600
776
  id={table.id}
601
777
  data-svelte-tably={table.cssId}
602
778
  class="table svelte-tably"
603
- style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
779
+ style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px; --scrollbar: {tbody.scrollbar}px; --viewport-width: {tbody.viewportWidth}px; --tably-context-width: {table.row?.options.context.alignHeaderToRows && contextWidth > 0 ? `${contextWidth}px` : (table.row?.options.context.width ?? 'max-content')};"
604
780
  aria-rowcount={table.data.length}
605
781
  >
606
782
  {#if columns.some((v) => v.snippets.header)}
@@ -620,23 +796,31 @@
620
796
  ],
621
797
  'header'
622
798
  )}
623
- {#if table.row?.snippets.contextHeader}
624
- <th class="context-col">
625
- {@render table.row?.snippets.contextHeader()}
799
+ {#if table.row?.snippets.context}
800
+ <th
801
+ class="context-col"
802
+ data-tably-context-measure={table.row?.options.context.alignHeaderToRows ? 'header' : undefined}
803
+ aria-hidden={table.row?.snippets.contextHeader ? undefined : true}
804
+ role={table.row?.snippets.contextHeader ? undefined : 'presentation'}
805
+ >
806
+ {#if table.row?.snippets.contextHeader}
807
+ <div class="context-inner">
808
+ {@render table.row?.snippets.contextHeader()}
809
+ </div>
810
+ {/if}
626
811
  </th>
627
812
  {/if}
628
813
  </tr>
629
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
630
814
  </thead>
631
815
  {/if}
632
816
 
633
817
  <tbody
634
818
  class="content"
635
819
  use:reorderArea={{ axis: 'y', class: table.cssId }}
820
+ use:observeScrollbar
636
821
  bind:this={virtualization.viewport.element}
637
822
  onscrollcapture={onscroll}
638
823
  bind:clientHeight={virtualization.viewport.height}
639
- bind:clientWidth={tbody.width}
640
824
  >
641
825
  {#if table.options.reorderable}
642
826
  {@render reorderArea({
@@ -655,6 +839,39 @@
655
839
  {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
656
840
  {/each}
657
841
  {/if}
842
+
843
+ {#if columns.length > 0 && virtualization.virtualTop === 0 && virtualization.virtualBottom === 0}
844
+ <tr class="filler" aria-hidden="true">
845
+ {#each fixed as column (column)}
846
+ {#if !hidden.includes(column)}
847
+ <td
848
+ class={`column sticky fixed ${column.options.class ?? ''}`}
849
+ data-column={column.id}
850
+ ></td>
851
+ {/if}
852
+ {/each}
853
+ {#each sticky as column, i (column)}
854
+ {#if !hidden.includes(column)}
855
+ <td
856
+ class={`column sticky ${column.options.class ?? ''}`}
857
+ class:border={i == sticky.length - 1}
858
+ data-column={column.id}
859
+ ></td>
860
+ {/if}
861
+ {/each}
862
+ {#each scrolled as column (column)}
863
+ {#if !hidden.includes(column)}
864
+ <td
865
+ class={`column ${column.options.class ?? ''}`}
866
+ data-column={column.id}
867
+ ></td>
868
+ {/if}
869
+ {/each}
870
+ {#if table.row?.snippets.context}
871
+ <td class="context-col" aria-hidden="true"></td>
872
+ {/if}
873
+ </tr>
874
+ {/if}
658
875
  </tbody>
659
876
 
660
877
  {#if columns.some((v) => v.snippets.statusbar)}
@@ -671,8 +888,10 @@
671
888
  ],
672
889
  'statusbar'
673
890
  )}
891
+ {#if table.row?.snippets.context}
892
+ <td class="context-col" aria-hidden="true"></td>
893
+ {/if}
674
894
  </tr>
675
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
676
895
  </tfoot>
677
896
  {/if}
678
897
 
@@ -708,11 +927,11 @@
708
927
  </caption>
709
928
  </table>
710
929
 
711
- {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
930
+ {#snippet headerSelected(ctx: HeaderSelectCtx<any>)}
712
931
  <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
713
932
  {/snippet}
714
933
 
715
- {#snippet rowSelected(ctx: RowSelectCtx<T>)}
934
+ {#snippet rowSelected(ctx: RowSelectCtx<any>)}
716
935
  <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
717
936
  {/snippet}
718
937
 
@@ -798,9 +1017,20 @@
798
1017
  })}
799
1018
  {/if}
800
1019
  {#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)}
1020
+ {@const expandId = getExpandId(item)}
1021
+ {@const expanded = row.expanded}
1022
+ {@const label = expanded ? 'Collapse row' : 'Expand row'}
1023
+ <button
1024
+ class="expand-row"
1025
+ tabindex="-1"
1026
+ type="button"
1027
+ aria-label={label}
1028
+ aria-expanded={expanded}
1029
+ aria-controls={expandId}
1030
+ onclick={() => (row.expanded = !row.expanded)}
1031
+ >
1032
+ {#if expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
1033
+ {@render chevronSnippet(expanded ? 180 : 90)}
804
1034
  {/if}
805
1035
  </button>
806
1036
  {/if}
@@ -811,12 +1041,12 @@
811
1041
  {/if}
812
1042
 
813
1043
  {#if table.options.auto}
814
- {#each Object.keys(table.data[0] || {}) as key}
1044
+ {#each autoSchema.keys as key}
815
1045
  <Column
816
1046
  id={key}
817
- value={(r) => r[key]}
1047
+ value={(r) => (r as any)?.[key]}
818
1048
  header={capitalize(segmentize(key))}
819
- sort={typeof table.data[0]?.[key] === 'number' ?
1049
+ sort={typeof autoSchema.sample?.[key] === 'number' ?
820
1050
  (a, b) => a - b
821
1051
  : (a, b) => String(a).localeCompare(String(b))}
822
1052
  />
@@ -847,24 +1077,57 @@
847
1077
  justify-content: center;
848
1078
  position: sticky;
849
1079
  right: 0;
850
- height: 100%;
851
1080
  z-index: 3;
852
1081
  padding: 0;
853
-
854
- &.hover {
855
- position: absolute;
856
- }
1082
+ border-left: 1px solid var(--tably-border);
857
1083
  &.hidden {
858
1084
  pointer-events: none;
859
1085
  user-select: none;
860
1086
  border-left: none;
861
- background: none;
862
- > :global(*) {
863
- opacity: 0;
1087
+ > .context-inner {
1088
+ visibility: hidden;
864
1089
  }
865
1090
  }
866
1091
  }
867
1092
 
1093
+ .context-inner {
1094
+ display: flex;
1095
+ align-items: center;
1096
+ justify-content: center;
1097
+ padding: calc(var(--tably-padding-y) / 2) 0;
1098
+ overflow: clip;
1099
+ width: 100%;
1100
+ }
1101
+
1102
+ .table::before {
1103
+ content: '';
1104
+ grid-area: headers;
1105
+ justify-self: end;
1106
+ align-self: stretch;
1107
+ width: var(--scrollbar, 0px);
1108
+ background-color: var(--tably-bg);
1109
+ border-bottom: 1px solid var(--tably-border);
1110
+ border-right: 1px solid var(--tably-border);
1111
+ margin-right: -1px;
1112
+ pointer-events: none;
1113
+ position: relative;
1114
+ z-index: 4;
1115
+ }
1116
+
1117
+ .table::after {
1118
+ content: '';
1119
+ grid-area: statusbar;
1120
+ justify-self: end;
1121
+ align-self: stretch;
1122
+ width: var(--scrollbar, 0px);
1123
+ background-color: var(--tably-statusbar);
1124
+ border-right: 1px solid var(--tably-border);
1125
+ margin-right: -1px;
1126
+ pointer-events: none;
1127
+ position: relative;
1128
+ z-index: 4;
1129
+ }
1130
+
868
1131
  :global(:root) {
869
1132
  --tably-color: hsl(0, 0%, 0%);
870
1133
  --tably-bg: hsl(0, 0%, 100%);
@@ -881,24 +1144,50 @@
881
1144
 
882
1145
  .svelte-tably {
883
1146
  position: relative;
884
- overflow: visible;
1147
+ overflow: hidden;
1148
+ border-collapse: collapse;
1149
+ border-spacing: 0;
885
1150
  }
886
1151
 
887
1152
  .expandable {
888
- position: relative;
889
-
890
1153
  & > td {
891
- position: sticky;
892
- left: 1px;
893
- > div {
894
- position: absolute;
895
- overflow: auto;
896
- top: -1.5px;
897
- left: 0;
898
- }
1154
+ padding: 0;
1155
+ border: none;
899
1156
  }
900
1157
  }
901
1158
 
1159
+ .expandable-cell {
1160
+ grid-column: 1 / -1;
1161
+ display: block;
1162
+ min-width: 0;
1163
+ width: 100%;
1164
+ }
1165
+
1166
+ .expandable-sticky {
1167
+ position: sticky;
1168
+ left: 0;
1169
+ width: var(--viewport-width, 100%);
1170
+ min-width: 0;
1171
+ display: block;
1172
+ background-color: var(--tably-bg);
1173
+ z-index: 1;
1174
+ }
1175
+
1176
+ .expandable-clip {
1177
+ overflow: hidden;
1178
+ width: 100%;
1179
+ background-color: var(--tably-bg);
1180
+ border-bottom: 1px solid var(--tably-border-grid);
1181
+ }
1182
+
1183
+ .expandable-content {
1184
+ overflow: auto;
1185
+ width: 100%;
1186
+ background-color: var(--tably-bg);
1187
+ box-sizing: border-box;
1188
+ min-width: 0;
1189
+ }
1190
+
902
1191
  .expand-row {
903
1192
  display: flex;
904
1193
  justify-content: center;
@@ -952,6 +1241,7 @@
952
1241
  align-items: center;
953
1242
  justify-content: center;
954
1243
  gap: 0.25rem;
1244
+ background-color: transparent;
955
1245
  position: absolute;
956
1246
  top: 0;
957
1247
  left: 0;
@@ -960,25 +1250,25 @@
960
1250
  width: 100%;
961
1251
  }
962
1252
 
1253
+ .__fixed > * {
1254
+ background-color: transparent;
1255
+ }
1256
+
963
1257
  thead {
964
1258
  position: relative;
965
1259
  }
966
1260
 
967
1261
  tbody::before,
968
- tbody::after,
969
- selects::before,
970
- selects::after {
1262
+ tbody::after {
971
1263
  content: '';
972
- display: grid;
973
- min-height: 100%;
1264
+ display: block;
1265
+ flex: 0 0 auto;
974
1266
  }
975
1267
 
976
- tbody::before,
977
- selects::before {
1268
+ tbody::before {
978
1269
  height: var(--t);
979
1270
  }
980
- tbody::after,
981
- selects::after {
1271
+ tbody::after {
982
1272
  height: var(--b);
983
1273
  }
984
1274
 
@@ -1046,7 +1336,10 @@
1046
1336
 
1047
1337
  .table {
1048
1338
  display: grid;
1049
- height: auto;
1339
+ width: 100%;
1340
+ min-width: 0;
1341
+ min-height: 0;
1342
+ height: 100%;
1050
1343
  max-height: 100%;
1051
1344
  position: relative;
1052
1345
 
@@ -1058,8 +1351,8 @@
1058
1351
  'rows panel'
1059
1352
  'statusbar panel';
1060
1353
 
1061
- grid-template-columns: auto min-content;
1062
- grid-template-rows: auto 1fr auto;
1354
+ grid-template-columns: 1fr min-content;
1355
+ grid-template-rows: auto minmax(0, 1fr) auto;
1063
1356
 
1064
1357
  border: 1px solid var(--tably-border);
1065
1358
  border-radius: var(--tably-radius);
@@ -1070,25 +1363,43 @@
1070
1363
  grid-area: headers;
1071
1364
  z-index: 2;
1072
1365
  overflow: hidden;
1366
+ min-width: 0;
1367
+ padding-right: var(--scrollbar, 0px);
1368
+ border-bottom: 1px solid var(--tably-border);
1073
1369
  }
1074
1370
 
1075
1371
  .headers > tr > .column {
1076
1372
  width: auto !important;
1077
- border-bottom: 1px solid var(--tably-border);
1078
1373
  }
1079
- .headers > tr > .column,
1080
- .headers > tr > .context-col {
1081
- border-bottom: 1px solid var(--tably-border);
1374
+ .headers > tr > .column:not(:first-child) {
1082
1375
  border-left: 1px solid var(--tably-border-grid);
1083
1376
  }
1084
1377
 
1378
+ .headers > tr > .context-col {
1379
+ border-left: 1px solid var(--tably-border);
1380
+ background-color: var(--tably-bg);
1381
+ }
1382
+
1085
1383
  .content {
1086
- display: grid;
1087
- grid-auto-rows: max-content;
1384
+ display: flex;
1385
+ flex-direction: column;
1386
+ min-width: 0;
1387
+ min-height: 0;
1088
1388
 
1089
1389
  grid-area: rows;
1090
1390
  scrollbar-width: thin;
1091
- overflow: auto;
1391
+ overflow-x: auto;
1392
+ overflow-y: scroll;
1393
+ }
1394
+
1395
+ .content > tr.row,
1396
+ .content > tr.expandable {
1397
+ flex: 0 0 auto;
1398
+ }
1399
+
1400
+ .content > tr.filler {
1401
+ flex: 1 0 0px;
1402
+ min-height: 0;
1092
1403
  }
1093
1404
 
1094
1405
  .statusbar {
@@ -1096,19 +1407,30 @@
1096
1407
  grid-area: statusbar;
1097
1408
  overflow: hidden;
1098
1409
  background-color: var(--tably-statusbar);
1410
+ min-width: 0;
1411
+ padding-right: var(--scrollbar, 0px);
1099
1412
  }
1100
1413
 
1101
1414
  .statusbar > tr > .column {
1102
1415
  border-top: 1px solid var(--tably-border);
1103
1416
  padding: calc(var(--tably-padding-y) / 2) 0;
1104
1417
  }
1418
+ .statusbar > tr > .context-col {
1419
+ border-top: 1px solid var(--tably-border);
1420
+ border-left: 1px solid var(--tably-border);
1421
+ }
1422
+
1423
+ .statusbar > tr > .context-col {
1424
+ background-color: var(--tably-statusbar);
1425
+ }
1105
1426
 
1106
1427
  .headers > tr,
1107
1428
  .row,
1429
+ .expandable,
1430
+ .filler,
1108
1431
  .statusbar > tr {
1109
1432
  display: grid;
1110
1433
  width: 100%;
1111
- height: 100%;
1112
1434
  min-width: max-content;
1113
1435
 
1114
1436
  & > .column {
@@ -1140,16 +1462,49 @@
1140
1462
  }
1141
1463
  }
1142
1464
 
1465
+ .row > .context-col {
1466
+ background-color: var(--tably-bg);
1467
+ }
1468
+
1469
+ .row > .context-col.hidden {
1470
+ background-color: transparent;
1471
+ }
1472
+
1143
1473
  :global(#runic-drag .row) {
1144
1474
  border: 1px solid var(--tably-border-grid);
1145
1475
  border-top: 2px solid var(--tably-border-grid);
1146
1476
  }
1147
1477
 
1148
- .row > * {
1478
+ .headers > tr > .column:not(:first-child),
1479
+ .row > .column:not(:first-child),
1480
+ .filler > .column:not(:first-child),
1481
+ .statusbar > tr > .column:not(:first-child) {
1149
1482
  border-left: 1px solid var(--tably-border-grid);
1483
+ }
1484
+
1485
+ .row,
1486
+ .filler {
1150
1487
  border-bottom: 1px solid var(--tably-border-grid);
1151
1488
  }
1152
1489
 
1490
+ .filler {
1491
+ pointer-events: none;
1492
+ user-select: none;
1493
+ background: none;
1494
+ }
1495
+
1496
+ .sr-only {
1497
+ position: absolute;
1498
+ width: 1px;
1499
+ height: 1px;
1500
+ padding: 0;
1501
+ margin: -1px;
1502
+ overflow: hidden;
1503
+ clip: rect(0, 0, 0, 0);
1504
+ white-space: nowrap;
1505
+ border: 0;
1506
+ }
1507
+
1153
1508
  .panel {
1154
1509
  position: relative;
1155
1510
  grid-area: panel;