svelte-tably 1.0.0-next.0 → 1.0.0-next.10

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.
@@ -0,0 +1,928 @@
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
10
+
11
+ <script module lang="ts">
12
+ export interface TableState<
13
+ T extends Record<PropertyKey, any> = Record<PropertyKey, any>
14
+ > {
15
+ columns: Record<string, ColumnState<T>>
16
+ panels: Record<string, TPanel<T>>
17
+ selected: T[] | null
18
+ sortby?: string
19
+ sortReverse: boolean
20
+ positions: {
21
+ sticky: string[]
22
+ scroll: string[]
23
+ hidden: string[]
24
+ toggle(key: string): void
25
+ }
26
+ readonly resizeable: boolean
27
+ readonly data: T[]
28
+ /** Rows become anchors */
29
+ readonly href?: (item: T) => string
30
+ addColumn(key: string, options: ColumnState<T>): void
31
+ removeColumn(key: string): void
32
+ }
33
+
34
+ export function getTableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>>() {
35
+ return getContext<TableState<T>>('svelte5-table')
36
+ }
37
+
38
+ export type HeaderSelectCtx<T = any> = {
39
+ isSelected: boolean
40
+ /** The list of selected items */
41
+ readonly selected: T[]
42
+ /**
43
+ * See [MDN :indeterminate](https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate)
44
+ */
45
+ readonly indeterminate: boolean
46
+ }
47
+
48
+ export type RowSelectCtx<T = any> = {
49
+ readonly item: T
50
+ readonly row: RowCtx<unknown>
51
+ data: T[]
52
+ isSelected: boolean
53
+ }
54
+ </script>
55
+
56
+ <script lang="ts">
57
+ import { getContext, onMount, setContext, tick, untrack, type Snippet } from 'svelte'
58
+ import Column, { type ColumnProps, type RowCtx, type ColumnState } from './Column.svelte'
59
+ import Panel, { PanelTween, type Panel as TPanel } from './Panel.svelte'
60
+ import { fly } from 'svelte/transition'
61
+ import { sineInOut } from 'svelte/easing'
62
+ import { on } from 'svelte/events'
63
+
64
+ type T = $$Generic<Record<PropertyKey, unknown>>
65
+
66
+ type ConstructorReturnType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer K ? K : never
67
+ type ConstructorParams<T extends new (...args: any[]) => any> = T extends new (...args: infer K) => any ? K : never
68
+
69
+ type ContentCtx<T extends Record<PropertyKey, unknown>> = {
70
+ Column: {
71
+ new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
72
+ <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
73
+ }
74
+ Panel: typeof Panel
75
+ readonly table: TableState<T>
76
+ readonly data: T[]
77
+ }
78
+
79
+ interface Props {
80
+ content: Snippet<[context: ContentCtx<T>]>
81
+
82
+ panel?: string
83
+ data?: T[]
84
+ id?: string
85
+ href?: (item: T) => string
86
+ /**
87
+ * Can you change the width of the columns?
88
+ * @default true
89
+ */
90
+ resizeable?: boolean
91
+
92
+ selected?: T[]
93
+ select?:
94
+ | boolean
95
+ | {
96
+ /**
97
+ * The style, in which the selection is shown
98
+ *
99
+ * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
100
+ * an inconsistency/limitation of matching the scroll between the selection div and the rows.
101
+ *
102
+ * @default 'column'
103
+ */
104
+ style?: 'column'
105
+ /**
106
+ * When to show the row-select, when not selected?
107
+ * @default 'hover'
108
+ */
109
+ show?: 'hover' | 'always' | 'never'
110
+ /**
111
+ * Custom snippet
112
+ */
113
+ headerSnippet?: Snippet<[context: HeaderSelectCtx]>
114
+ rowSnippet?: Snippet<[context: RowSelectCtx<T>]>
115
+ }
116
+ // | {
117
+ // /**
118
+ // * The style, in which the selection is shown
119
+ // *
120
+ // * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
121
+ // * an inconsistency/limitation of matching the scroll between the selection div and the rows.
122
+ // *
123
+ // * @default 'column'
124
+ // */
125
+ // style?: 'edge' | 'side'
126
+ // /**
127
+ // * When to show the row-select, when not selected?
128
+ // * @default 'hover'
129
+ // */
130
+ // show?: 'hover'
131
+ // /**
132
+ // * Custom snippet
133
+ // */
134
+ // snippet?: Snippet<[context: { item: T, data: T[], selected: boolean }]>
135
+ // }
136
+
137
+ /*
138
+ ordered?: {
139
+ style?: 'column' | 'side' // combine with select if both use 'column'
140
+ show?: 'hover' | 'always'
141
+ // snippet?: Snippet<[context: { item: T, data: T[], selected: boolean }]>
142
+ }
143
+ */
144
+ }
145
+
146
+ let {
147
+ content,
148
+ selected = $bindable([]),
149
+ panel = $bindable(),
150
+ data: _data = [],
151
+ id = Array.from({ length: 12 }, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join(''),
152
+ href,
153
+ resizeable = true,
154
+ select
155
+ }: Props = $props()
156
+
157
+ let mounted = $state(false)
158
+ onMount(() => (mounted = true))
159
+
160
+ const data = $derived([..._data])
161
+
162
+ const elements = $state({}) as Record<
163
+ 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
164
+ HTMLElement
165
+ >
166
+
167
+ let cols: TableState<T>['columns'] = $state({})
168
+ let positions: TableState<T>['positions'] = $state({
169
+ fixed: [],
170
+ sticky: [],
171
+ scroll: [],
172
+ hidden: [],
173
+ toggle(key) {
174
+ if (table.positions.hidden.includes(key))
175
+ table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
176
+ else table.positions.hidden.push(key)
177
+ }
178
+ })
179
+
180
+ const table: TableState<T> = $state({
181
+ columns: cols,
182
+ selected,
183
+ panels: {},
184
+ positions,
185
+ sortReverse: false,
186
+ get href() {
187
+ return href
188
+ },
189
+ get data() {
190
+ return data
191
+ },
192
+ get resizeable() {
193
+ return resizeable
194
+ },
195
+ addColumn(key, column) {
196
+ table.columns[key] = column
197
+
198
+ if (column.defaults.sort) sortBy(key)
199
+
200
+ if (column.fixed) {
201
+ // @ts-expect-error
202
+ table.positions.fixed.push(key)
203
+ return
204
+ }
205
+
206
+ if (!column.defaults.show) table.positions.hidden.push(key)
207
+
208
+ if (column.defaults.sticky) table.positions.sticky.push(key)
209
+ else table.positions.scroll.push(key)
210
+ },
211
+ removeColumn(key) {
212
+ delete table.columns[key]
213
+ // @ts-expect-error fixed is not typed
214
+ table.positions.fixed = table.positions.fixed.filter((column) => column !== key)
215
+ table.positions.sticky = table.positions.sticky.filter((column) => column !== key)
216
+ table.positions.scroll = table.positions.scroll.filter((column) => column !== key)
217
+ table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
218
+ }
219
+ })
220
+
221
+ setContext('svelte5-table', table)
222
+
223
+ // * --- *
224
+
225
+ // * --- Virtualization --- *
226
+ // #region Virtualization
227
+ let scrollTop = $state(0)
228
+ let viewportHeight = $state(0)
229
+
230
+ let heightPerItem = $state(8)
231
+
232
+ const spacing = () => viewportHeight / 2
233
+
234
+ let virtualTop = $derived.by(() => {
235
+ let result = Math.max(scrollTop - spacing(), 0)
236
+ result -= result % heightPerItem
237
+ return result
238
+ })
239
+ let virtualBottom = $derived.by(() => {
240
+ let result = heightPerItem * data.length - virtualTop - spacing() * 4
241
+ result = Math.max(result, 0)
242
+ return result
243
+ })
244
+
245
+ let renderItemLength = $derived(Math.ceil(Math.max(30, (viewportHeight / heightPerItem) * 2)))
246
+
247
+ /** The area of data being rendered */
248
+ let area = $derived.by(() => {
249
+ table.sortReverse
250
+ table.sortby
251
+ const index = virtualTop / heightPerItem || 0
252
+ const end = index + renderItemLength
253
+ const result = data.slice(index, end)
254
+ return result
255
+ })
256
+
257
+ function calculateHeightPerItem() {
258
+ if (!elements.rows) {
259
+ heightPerItem = 8
260
+ return
261
+ }
262
+ tick().then(() => {
263
+ const firstRow = elements.rows.children[0].getBoundingClientRect().top
264
+ const lastRow =
265
+ elements.rows.children[elements.rows.children.length - 1].getBoundingClientRect().bottom
266
+ heightPerItem = (lastRow - firstRow) / area.length
267
+ })
268
+ }
269
+
270
+ $effect(() => {
271
+ data
272
+ untrack(calculateHeightPerItem)
273
+ })
274
+ // #endregion
275
+ // * --- Virtualization --- *
276
+
277
+
278
+
279
+ function sortBy(column: string) {
280
+ const { sorting, value } = table.columns[column]!.options
281
+ if(!sorting || !value) return
282
+
283
+ if (table.sortby === column) {
284
+ table.sortReverse = !table.sortReverse
285
+ }
286
+ else {
287
+ table.sortReverse = false
288
+ table.sortby = column
289
+ }
290
+ }
291
+ function sortAction(node: HTMLElement, column: string) {
292
+ $effect(() => on(node, 'click', () => sortBy(column)))
293
+ }
294
+
295
+ function sortTable() {
296
+ if (!table.sortby) return
297
+ const column = table.columns[table.sortby]
298
+ let { sorting, value } = column.options
299
+ if(!sorting || !value) return
300
+ if(sorting === true) {
301
+ sorting = (a, b) => String(a).localeCompare(String(b))
302
+ }
303
+ if(table.sortReverse) {
304
+ data.sort((a, b) => sorting(value(b), value(a)))
305
+ } else {
306
+ data.sort((a, b) => sorting(value(a), value(b)))
307
+ }
308
+ }
309
+
310
+ $effect.pre(() => {
311
+ data
312
+ table.sortby
313
+ table.sortReverse
314
+ untrack(sortTable)
315
+ })
316
+
317
+ const panelTween = new PanelTween(() => panel, 24)
318
+
319
+ let hoveredRow: T | null = $state(null)
320
+
321
+ /** Order of columns */
322
+ const fixed = $derived(
323
+ // @ts-expect-error
324
+ positions.fixed
325
+ ) as string[]
326
+ const hidden = $derived(positions.hidden)
327
+ const notHidden = (key: string) => !positions.hidden.includes(key)
328
+ const sticky = $derived(positions.sticky.filter(notHidden))
329
+ const scrolled = $derived(positions.scroll.filter(notHidden))
330
+ const columns = $derived([...fixed, ...sticky, ...scrolled])
331
+
332
+ /** Width of each column */
333
+ const columnWidths = $state({}) as Record<string, number>
334
+
335
+ const getWidth = (key: string, def: number = 150) =>
336
+ columnWidths[key] || table.columns[key]?.defaults.width || def
337
+
338
+ /** grid-template-columns for widths */
339
+ const style = $derived.by(() => {
340
+ if (!mounted) return ''
341
+ const templateColumns = `
342
+ #${id} > .headers,
343
+ #${id} > tbody > .row,
344
+ #${id} > tfoot > tr,
345
+ #${id} > .content > .virtual.bottom {
346
+ grid-template-columns: ${columns
347
+ .map((key, i, arr) => {
348
+ const width = getWidth(key)
349
+ if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
350
+ return `${width}px`
351
+ })
352
+ .join(' ')};
353
+ }
354
+ `
355
+
356
+ let sum = 0
357
+ const stickyLeft = [...fixed, ...sticky]
358
+ .map((key, i, arr) => {
359
+ sum += getWidth(arr[i - 1], i === 0 ? 0 : undefined)
360
+ return `
361
+ #${id} .column.sticky[data-column='${key}'] {
362
+ left: ${sum}px;
363
+ }
364
+ `
365
+ })
366
+ .join('')
367
+
368
+ return templateColumns + stickyLeft
369
+ })
370
+
371
+ function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
372
+ if (!isHeader) return
373
+
374
+ const key = node.getAttribute('data-column')!
375
+ node.style.width = getWidth(key) + 'px'
376
+
377
+ let mouseup = false
378
+
379
+ const observer = new MutationObserver(() => {
380
+ const width = parseFloat(node.style.width)
381
+ if(width === columnWidths[key]) return
382
+ columnWidths[key] = width
383
+ if(!mouseup) {
384
+ mouseup = true
385
+ window.addEventListener('click', (e) => {
386
+ e.preventDefault()
387
+ e.stopPropagation()
388
+ mouseup = false
389
+ }, { once: true, capture: true })
390
+ }
391
+ })
392
+
393
+ observer.observe(node, { attributes: true })
394
+ return { destroy: () => observer.disconnect() }
395
+ }
396
+
397
+ async function onscroll() {
398
+ const target = elements.rows
399
+ if (target.scrollTop !== scrollTop) {
400
+ scrollTop = target?.scrollTop ?? scrollTop
401
+ }
402
+
403
+ if (elements.selects) {
404
+ elements.selects.scrollTop = target?.scrollTop
405
+ }
406
+
407
+ if (!elements.headers) return
408
+ elements.headers.scrollLeft = target.scrollLeft
409
+ elements.statusbar.scrollLeft = target.scrollLeft
410
+ }
411
+
412
+ export { selected, positions, data, href, cols as columns }
413
+ </script>
414
+
415
+ <!---------------------------------------------------->
416
+
417
+ <svelte:head>
418
+ {@html `<style>${style}</style>`}
419
+ </svelte:head>
420
+
421
+ {#snippet chevronSnippet(reversed: boolean)}
422
+ <svg
423
+ class='sorting-icon'
424
+ class:reversed
425
+ xmlns="http://www.w3.org/2000/svg"
426
+ width="16"
427
+ height="16"
428
+ viewBox="0 0 16 16"
429
+ style='margin: auto; margin-right: var(--tably-padding-x, 1rem);'
430
+ >
431
+ <path
432
+ fill="currentColor"
433
+ d="M3.2 5.74a.75.75 0 0 1 1.06-.04L8 9.227L11.74 5.7a.75.75 0 1 1 1.02 1.1l-4.25 4a.75.75 0 0 1-1.02 0l-4.25-4a.75.75 0 0 1-.04-1.06"
434
+ />
435
+ </svg>
436
+ {/snippet}
437
+
438
+ {#snippet columnsSnippet(
439
+ renderable: (column: string) => Snippet<[arg0?: any, arg1?: any]> | undefined,
440
+ arg: null | ((column: string) => any[]) = null,
441
+ isHeader = false
442
+ )}
443
+ {#each fixed as column, i (column)}
444
+ {#if !hidden.includes(column)}
445
+ {@const args = arg ? arg(column) : []}
446
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
447
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
448
+ <svelte:element
449
+ this={isHeader ? 'th' : 'td'}
450
+ class="column sticky fixed"
451
+ data-column={column}
452
+ class:header={isHeader}
453
+ class:sortable={sortable}
454
+ use:sortClick={column}
455
+ >
456
+ {@render renderable(column)?.(args[0], args[1])}
457
+ {#if isHeader && table.sortby === column && sortable}
458
+ {@render chevronSnippet(table.sortReverse)}
459
+ {/if}
460
+ </svelte:element>
461
+ {/if}
462
+ {/each}
463
+ {#each sticky as column, i (column)}
464
+ {#if !hidden.includes(column)}
465
+ {@const args = arg ? arg(column) : []}
466
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
467
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
468
+ <svelte:element
469
+ this={isHeader ? 'th' : 'td'}
470
+ class="column sticky"
471
+ use:observeColumnWidth={isHeader}
472
+ data-column={column}
473
+ class:header={isHeader}
474
+ class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
475
+ class:border={i == sticky.length - 1}
476
+ class:sortable={sortable}
477
+ use:sortClick={column}
478
+ >
479
+ {@render renderable(column)?.(args[0], args[1])}
480
+ {#if isHeader && table.sortby === column && sortable}
481
+ {@render chevronSnippet(table.sortReverse)}
482
+ {/if}
483
+ </svelte:element>
484
+ {/if}
485
+ {/each}
486
+ {#each scrolled as column, i (column)}
487
+ {#if !hidden.includes(column)}
488
+ {@const args = arg ? arg(column) : []}
489
+ {@const sortable = isHeader && table.columns[column]!.options.sorting}
490
+ {@const sortClick = isHeader ? sortAction : ()=>{}}
491
+ <svelte:element
492
+ this={isHeader ? 'th' : 'td'}
493
+ class="column"
494
+ data-column={column}
495
+ use:observeColumnWidth={isHeader}
496
+ class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
497
+ class:sortable={sortable}
498
+ use:sortClick={column}
499
+ >
500
+ {@render renderable(column)?.(args[0], args[1])}
501
+ {#if isHeader && table.sortby === column && sortable}
502
+ {@render chevronSnippet(table.sortReverse)}
503
+ {/if}
504
+ </svelte:element>
505
+ {/if}
506
+ {/each}
507
+ {/snippet}
508
+
509
+ <table
510
+ {id}
511
+ class="table svelte-tably"
512
+ style="--t: {virtualTop}px; --b: {virtualBottom}px;"
513
+ aria-rowcount={data.length}
514
+ >
515
+ <thead class="headers" bind:this={elements.headers}>
516
+ {@render columnsSnippet(
517
+ (column) => table.columns[column]?.header,
518
+ () => [true],
519
+ true
520
+ )}
521
+ </thead>
522
+
523
+ <tbody class="content" bind:this={elements.rows} onscrollcapture={onscroll} bind:clientHeight={viewportHeight}>
524
+ {#each area as item, i (item)}
525
+ {@const props = table.href ? { href: table.href(item) } : {}}
526
+ {@const index = data.indexOf(item) + 1}
527
+ <svelte:element
528
+ this={table.href ? 'a' : 'tr'}
529
+ class="row"
530
+ class:hover={hoveredRow === item}
531
+ class:selected={table.selected?.includes(item)}
532
+ class:first={i === 0}
533
+ class:last={i === area.length - 1}
534
+ {...props}
535
+ aria-rowindex={index}
536
+ onpointerenter={() => (hoveredRow = item)}
537
+ onpointerleave={() => (hoveredRow = null)}
538
+ >
539
+ {@render columnsSnippet(
540
+ (column) => table.columns[column]!.row,
541
+ (column) => {
542
+ const col = table.columns[column]!
543
+ return [
544
+ item,
545
+ {
546
+ get index() {
547
+ return index - 1
548
+ },
549
+ get value() {
550
+ return col.options.value ? col.options.value(item) : undefined
551
+ },
552
+ get isHovered() {
553
+ return hoveredRow === item
554
+ },
555
+ get selected() {
556
+ return table.selected?.includes(item)
557
+ },
558
+ set selected(value) {
559
+ value ?
560
+ table.selected!.push(item)
561
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
562
+ }
563
+ }
564
+ ]
565
+ }
566
+ )}
567
+ </svelte:element>
568
+ {/each}
569
+ </tbody>
570
+
571
+ <tfoot class="statusbar" bind:this={elements.statusbar}>
572
+ <tr>
573
+ {@render columnsSnippet((column) => table.columns[column]?.statusbar)}
574
+ </tr>
575
+ </tfoot>
576
+
577
+ <caption
578
+ class="panel"
579
+ style="width: {panelTween.current}px;"
580
+ style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}
581
+ >
582
+ {#if panel && panel in table.panels}
583
+ <div
584
+ class="panel-content"
585
+ bind:clientWidth={panelTween.width}
586
+ in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
587
+ out:fly={{ x: 100, duration: 200, easing: sineInOut }}
588
+ >
589
+ {@render table.panels[panel].content({
590
+ get table() {
591
+ return table
592
+ },
593
+ get data() {
594
+ return data
595
+ }
596
+ })}
597
+ </div>
598
+ {/if}
599
+ </caption>
600
+ <caption class="backdrop" aria-hidden={panel && table.panels[panel]?.backdrop ? false : true}>
601
+ <button aria-label="Panel backdrop" class='btn-backdrop' tabindex="-1" onclick={() => (panel = undefined)}></button>
602
+ </caption>
603
+ </table>
604
+
605
+ {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
606
+ <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
607
+ {/snippet}
608
+
609
+ {#snippet rowSelected(ctx: RowSelectCtx<T>)}
610
+ <input type="checkbox" bind:checked={ctx.isSelected} />
611
+ {/snippet}
612
+
613
+ {#if select}
614
+ {@const {
615
+ show = 'hover',
616
+ style = 'column',
617
+ rowSnippet = rowSelected,
618
+ headerSnippet = headerSelected
619
+ } = typeof select === 'boolean' ? {} : select}
620
+ {#if show !== 'never'}
621
+ <Column id="__fixed" {table} fixed width={56} resizeable={false}>
622
+ {#snippet header()}
623
+ <div class="__fixed">
624
+ {@render headerSnippet({
625
+ get isSelected() {
626
+ return table.data.length === table.selected?.length
627
+ },
628
+ set isSelected(value) {
629
+ if (value) {
630
+ table.selected = table.data
631
+ } else {
632
+ table.selected = []
633
+ }
634
+ },
635
+ get selected() {
636
+ return table.selected!
637
+ },
638
+ get indeterminate() {
639
+ return (table.selected?.length || 0) > 0 && table.data.length !== table.selected?.length
640
+ }
641
+ })}
642
+ </div>
643
+ {/snippet}
644
+ {#snippet row(item, row)}
645
+ <div class="__fixed">
646
+ {#if row.selected || show === 'always' || (row.isHovered && show === 'hover')}
647
+ {@render rowSnippet({
648
+ get isSelected() {
649
+ return row.selected
650
+ },
651
+ set isSelected(value) {
652
+ row.selected = value
653
+ },
654
+ get row() {
655
+ return row
656
+ },
657
+ get item() {
658
+ return item
659
+ },
660
+ get data() {
661
+ return table.data
662
+ }
663
+ })}
664
+ {/if}
665
+ </div>
666
+ {/snippet}
667
+ </Column>
668
+ {/if}
669
+ {/if}
670
+
671
+ {@render content?.({
672
+ Column,
673
+ Panel,
674
+ get table() {
675
+ return table
676
+ },
677
+ get data() {
678
+ return data
679
+ }
680
+ })}
681
+
682
+ <!---------------------------------------------------->
683
+ <style>
684
+ .svelte-tably *,
685
+ .svelte-tably {
686
+ box-sizing: border-box;
687
+ background-color: inherit;
688
+ }
689
+
690
+ .svelte-tably {
691
+ position: relative;
692
+ overflow: visible;
693
+ }
694
+
695
+ caption {
696
+ all: unset;
697
+ }
698
+
699
+ input[type='checkbox'] {
700
+ width: 18px;
701
+ height: 18px;
702
+ cursor: pointer;
703
+ }
704
+
705
+ button.btn-backdrop {
706
+ outline: none;
707
+ border: none;
708
+ cursor: pointer;
709
+ }
710
+
711
+ .sorting-icon {
712
+ transition: transform .15s ease;
713
+ transform: rotateZ(0deg);
714
+ &.reversed {
715
+ transform: rotateZ(-180deg);
716
+ }
717
+ }
718
+
719
+ .__fixed {
720
+ display: flex;
721
+ align-items: center;
722
+ justify-content: center;
723
+ gap: 0.5rem;
724
+ position: absolute;
725
+ top: 0;
726
+ left: 0;
727
+ right: 0;
728
+ bottom: 0;
729
+ width: 100%;
730
+ }
731
+
732
+ .first .__fixed {
733
+ top: var(--tably-padding-y, 0.5rem);
734
+ }
735
+ .last .__fixed {
736
+ bottom: var(--tably-padding-y, 0.5rem);
737
+ }
738
+
739
+ tbody::before,
740
+ tbody::after,
741
+ selects::before,
742
+ selects::after {
743
+ content: '';
744
+ display: grid;
745
+ min-height: 100%;
746
+ }
747
+
748
+ tbody::before,
749
+ selects::before {
750
+ height: var(--t);
751
+ }
752
+ tbody::after,
753
+ selects::after {
754
+ height: var(--b);
755
+ }
756
+
757
+ a.row {
758
+ color: inherit;
759
+ text-decoration: inherit;
760
+ }
761
+
762
+ .backdrop {
763
+ position: absolute;
764
+ left: 0px;
765
+ top: 0px;
766
+ bottom: 0px;
767
+ right: 0px;
768
+ background-color: hsla(0, 0%, 0%, 0.3);
769
+ z-index: 3;
770
+ opacity: 1;
771
+ transition: 0.15s ease;
772
+ border: none;
773
+ outline: none;
774
+ cursor: pointer;
775
+
776
+ > button {
777
+ position: absolute;
778
+ left: 0px;
779
+ top: 0px;
780
+ bottom: 0px;
781
+ right: 0px;
782
+ }
783
+
784
+ &[aria-hidden='true'] {
785
+ opacity: 0;
786
+ pointer-events: none;
787
+ }
788
+ }
789
+
790
+ .headers,
791
+ .statusbar {
792
+ /* So that the scrollbar doesn't cause the headers/statusbar to shift */
793
+ padding-right: 11px;
794
+ }
795
+
796
+ .table {
797
+ color: var(--tably-color, hsl(0, 0%, 0%));
798
+ background-color: var(--tably-bg, hsl(0, 0%, 100%));
799
+ }
800
+
801
+ .sticky {
802
+ position: sticky;
803
+ /* right: 100px; */
804
+ z-index: 1;
805
+ }
806
+
807
+ .sticky.border {
808
+ border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
809
+ }
810
+
811
+ .headers > .column {
812
+ border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
813
+ overflow: hidden;
814
+ padding: var(--tably-padding-y, 0.5rem) 0;
815
+ cursor: default;
816
+ user-select: none;
817
+
818
+ &.sortable {
819
+ cursor: pointer;
820
+ }
821
+
822
+ &.resizeable {
823
+ resize: horizontal;
824
+ }
825
+ }
826
+
827
+ .table {
828
+ display: grid;
829
+ height: 100%;
830
+ position: relative;
831
+
832
+ grid-template-areas:
833
+ 'headers panel'
834
+ 'rows panel'
835
+ 'statusbar panel';
836
+
837
+ grid-template-columns: auto min-content;
838
+ grid-template-rows: auto 1fr auto;
839
+
840
+ border: 1px solid var(--tably-border, hsl(0, 0%, 90%));
841
+ border-radius: var(--tably-radius, 0.25rem);
842
+
843
+ max-height: 100%;
844
+ }
845
+
846
+ .headers {
847
+ grid-area: headers;
848
+ z-index: 2;
849
+ overflow: hidden;
850
+ }
851
+
852
+ .headers > .column {
853
+ width: auto !important;
854
+ border-bottom: 1px solid var(--tably-border, hsl(0, 0%, 90%));
855
+ }
856
+
857
+ .content {
858
+ display: grid;
859
+ grid-auto-rows: max-content;
860
+
861
+ grid-area: rows;
862
+ scrollbar-width: thin;
863
+ overflow: auto;
864
+ /* height: 100%; */
865
+ }
866
+
867
+ .statusbar {
868
+ grid-area: statusbar;
869
+ overflow: hidden;
870
+ background-color: var(--tably-statusbar, hsl(0, 0%, 98%));
871
+ }
872
+
873
+ .statusbar > tr > .column {
874
+ border-top: 1px solid var(--tably-border, hsl(0, 0%, 90%));
875
+ padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
876
+ }
877
+
878
+ .headers,
879
+ .row,
880
+ .statusbar > tr {
881
+ position: relative;
882
+ display: grid;
883
+ width: 100%;
884
+ height: 100%;
885
+
886
+ & > .column {
887
+ display: flex;
888
+ padding-left: var(--tably-padding-x, 1rem);
889
+ overflow: hidden;
890
+ }
891
+
892
+ & > *:last-child {
893
+ width: 100%;
894
+ padding-right: var(--tably-padding-x, 1rem);
895
+ }
896
+ }
897
+
898
+ .row:first-child > * {
899
+ padding-top: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
900
+ }
901
+ .row:last-child > * {
902
+ padding-bottom: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
903
+ }
904
+
905
+ .row > * {
906
+ padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
907
+ }
908
+
909
+ .panel {
910
+ position: relative;
911
+ grid-area: panel;
912
+ height: 100%;
913
+
914
+ border-left: 1px solid var(--tably-border, hsl(0, 0%, 90%));
915
+ scrollbar-gutter: stable both-edges;
916
+ scrollbar-width: thin;
917
+ z-index: 4;
918
+
919
+ > .panel-content {
920
+ position: absolute;
921
+ top: 0;
922
+ right: 0;
923
+ width: min-content;
924
+ overflow: auto;
925
+ padding: var(--tably-padding-y, 0.5rem) 0;
926
+ }
927
+ }
928
+ </style>