svelte-tably 1.0.0-next.11 → 1.0.0-next.13

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