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

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