svelte-tably 1.0.0 → 1.0.1

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.
@@ -1,1139 +1,1140 @@
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
- </script>
13
-
14
- <script lang="ts">
15
- import { type Snippet } from 'svelte'
16
- import { fly } from 'svelte/transition'
17
- import { sineInOut } from 'svelte/easing'
18
- import reorder, { type ItemState } from 'runic-reorder'
19
- import { Virtualization } from './virtualization.svelte.js'
20
- import {
21
- TableState,
22
- type HeaderSelectCtx,
23
- type RowCtx,
24
- type RowSelectCtx,
25
- type TableProps
26
- } from './table.svelte.js'
27
- import Panel from '../panel/Panel.svelte'
28
- import Column from '../column/Column.svelte'
29
- import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
30
- import { conditional } from '../conditional.svelte.js'
31
- import { ColumnState, type RowColumnCtx } from '../column/column.svelte.js'
32
- import Expandable from '../expandable/Expandable.svelte'
33
- import { SizeTween } from '../size-tween.svelte.js'
34
- import { on } from 'svelte/events'
35
- import Row from '../row/Row.svelte'
36
-
37
- type T = $$Generic<Record<PropertyKey, unknown>>
38
-
39
- type ConstructorReturnType<T extends new (...args: any[]) => any> =
40
- T extends new (...args: any[]) => infer K ? K : never
41
- type ConstructorParams<T extends new (...args: any[]) => any> =
42
- T extends new (...args: infer K) => any ? K : never
43
-
44
- type ContentCtx<T extends Record<PropertyKey, unknown>> = {
45
- Column: {
46
- new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
47
- <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
48
- }
49
- Panel: typeof Panel<T>
50
- Expandable: typeof Expandable<T>
51
- Row: typeof Row<T>
52
- readonly table: TableState<T>
53
- }
54
-
55
- type ContentSnippet = Snippet<[context: ContentCtx<T>]>
56
-
57
- let {
58
- content,
59
- selected: _selected = $bindable([]),
60
- panel: _panel = $bindable(),
61
- data: _data = $bindable([]),
62
- ...restProps
63
- }: TableProps<T> & { content?: ContentSnippet } = $props()
64
-
65
- const properties = fromProps(restProps, {
66
- selected: [() => _selected, (v) => (_selected = v)],
67
- panel: [() => _panel, (v) => (_panel = v)],
68
- data: [() => _data, (v) => (_data = v)]
69
- }) as TableProps<T>
70
-
71
- const mount = mounted()
72
-
73
- const reorderArea = reorder(rowSnippet)
74
-
75
- const elements = $state({}) as Record<
76
- 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
77
- HTMLElement
78
- >
79
-
80
- const table = new TableState<T>(properties) as TableState<T>
81
-
82
- const virtualization = new Virtualization(table)
83
-
84
- const panelTween = new SizeTween(() => !!properties.panel)
85
-
86
- let hoveredRow: T | null = $state(null)
87
- let hoveredColumn: ColumnState | null = $state(null)
88
-
89
- /** Order of columns */
90
- const fixed = $derived(table.positions.fixed)
91
- const hidden = $derived(table.positions.hidden)
92
- const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
93
- const sticky = $derived(table.positions.sticky.filter(notHidden))
94
- const scrolled = $derived(table.positions.scroll.filter(notHidden))
95
- const columns = $derived([...fixed, ...sticky, ...scrolled])
96
-
97
- /** Width of each column */
98
- const columnWidths = $state({}) as Record<string, number>
99
-
100
- const getWidth = (key: string, def: number = 150) =>
101
- columnWidths[key] || table.columns[key]?.defaults.width || def
102
-
103
- /** grid-template-columns for widths */
104
- const style = $derived.by(() => {
105
- if (!mount.isMounted) return ''
106
-
107
- const context = table.row?.snippets.context ? table.row?.options.context.width : ''
108
-
109
- const templateColumns =
110
- columns
111
- .map((column, i, arr) => {
112
- const width = getWidth(column.id)
113
- if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
114
- return `${width}px`
115
- })
116
- .join(' ') + context
117
-
118
- const theadTempla3teColumns = `
119
- #${table.id} > thead > tr,
120
- #${table.id} > tfoot > tr {
121
- grid-template-columns: ${templateColumns};
122
- }
123
- `
124
-
125
- const tbodyTemplateColumns = `
126
- [data-area-class='${table.id}'] tr.row,
127
- #${table.id} > tbody::after {
128
- grid-template-columns: ${templateColumns};
129
- }
130
- `
131
-
132
- let sum = 0
133
- const stickyLeft = [...fixed, ...sticky]
134
- .map((column, i, arr) => {
135
- sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
136
- return `
137
- #${table.id} .column.sticky[data-column='${column.id}'],
138
- [data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
139
- left: ${sum}px;
140
- }
141
- `
142
- })
143
- .join('')
144
-
145
- const columnStyling = columns
146
- .map((column) =>
147
- !column.options.style ?
148
- ''
149
- : `
150
- [data-area-class='${table.id}'] .column[data-column='${column.id}'] {
151
- ${column.options.style}
152
- }
153
- `
154
- )
155
- .join('')
156
-
157
- return theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
158
- })
159
-
160
- function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
161
- if (!isHeader) return
162
-
163
- const key = node.getAttribute('data-column')!
164
- node.style.width = getWidth(key) + 'px'
165
-
166
- let mouseup = false
167
-
168
- const observer = new MutationObserver(() => {
169
- const width = parseFloat(node.style.width)
170
- if (width === columnWidths[key]) return
171
- columnWidths[key] = width
172
- if (!mouseup) {
173
- mouseup = true
174
- window.addEventListener(
175
- 'click',
176
- (e) => {
177
- e.preventDefault()
178
- e.stopPropagation()
179
- mouseup = false
180
- },
181
- { once: true, capture: true }
182
- )
183
- }
184
- })
185
-
186
- observer.observe(node, { attributes: true })
187
- return { destroy: () => observer.disconnect() }
188
- }
189
-
190
- let tbody = $state({
191
- width: 0
192
- })
193
- async function onscroll() {
194
- const target = virtualization.viewport.element!
195
- if (target.scrollTop !== virtualization.scrollTop) {
196
- virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
197
- }
198
-
199
- if (elements.selects) {
200
- elements.selects.scrollTop = target?.scrollTop
201
- }
202
-
203
- if (!elements.headers) return
204
- elements.headers.scrollLeft = target.scrollLeft
205
- elements.statusbar.scrollLeft = target.scrollLeft
206
- }
207
-
208
- // * --- CSV --- *
209
- let csv = $state(false) as false | { selected?: boolean }
210
- let csvElement = $state() as undefined | HTMLTableElement
211
- interface CSVOptions {
212
- /** Semi-colons as separator? */
213
- semicolon?: boolean
214
- /** Only selected rows */
215
- selected?: boolean
216
- }
217
- export async function toCSV(opts: CSVOptions = {}) {
218
- csv = { selected: !!opts.selected }
219
- let resolve: (value: HTMLTableElement) => void
220
- const promise = new Promise<HTMLTableElement>((r) => (resolve = r))
221
-
222
- const clean = $effect.root(() => {
223
- $effect(() => {
224
- if (csvElement) {
225
- resolve(csvElement)
226
- }
227
- })
228
- })
229
-
230
- let table = await promise
231
- clean()
232
-
233
- const separator = opts.semicolon ? ';' : ','
234
- const rows = Array.from(table.rows)
235
- const csvRows = [] as string[]
236
-
237
- for (const row of rows) {
238
- const cells = Array.from(row.cells)
239
- const csvCells = cells.map((cell) => {
240
- let text = cell.textContent?.trim() || ''
241
-
242
- // Escape double quotes and wrap in quotes if needed
243
- if (text.includes('"')) {
244
- text = text.replace(/"/g, '""')
245
- }
246
- if (text.includes(separator) || text.includes('"') || text.includes('\n')) {
247
- text = `"${text}"`
248
- }
249
-
250
- return text
251
- })
252
- csvRows.push(csvCells.join(separator))
253
- }
254
-
255
- csv = false
256
- return csvRows.join('\n')
257
- }
258
- // * --- CSV --- *
259
-
260
- let expandedRow = $state([]) as T[]
261
- let expandTick = false
262
- function toggleExpand(item: T, value?: boolean) {
263
- if (expandTick) return
264
- expandTick = true
265
- requestAnimationFrame(() => (expandTick = false))
266
-
267
- let indexOf = expandedRow.indexOf(item)
268
- if (value === undefined) {
269
- value = indexOf === -1
270
- }
271
- if (!value) {
272
- expandedRow.splice(indexOf, 1)
273
- return
274
- }
275
- if (table.expandable?.options.multiple === true) {
276
- expandedRow.push(item)
277
- } else {
278
- expandedRow[0] = item
279
- }
280
- }
281
-
282
- function addRowColumnEvents(
283
- node: HTMLTableColElement,
284
- opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
285
- ) {
286
- const [where, column, value] = opts
287
- if (where !== 'row') return
288
- if (column.options.onclick) {
289
- $effect(() => on(node, 'click', (e) => column.options.onclick!(e, value())))
290
- }
291
- }
292
-
293
- function addRowEvents(node: HTMLTableRowElement, ctx: RowCtx<T>) {
294
- if (table.row?.events.onclick) {
295
- $effect(() => on(node, 'click', (e) => table.row?.events.onclick!(e, ctx)))
296
- }
297
- if (table.row?.events.oncontextmenu) {
298
- $effect(() => on(node, 'contextmenu', (e) => table.row?.events.oncontextmenu!(e, ctx)))
299
- }
300
- }
301
- </script>
302
-
303
- <!---------------------------------------------------->
304
-
305
- {#if csv !== false}
306
- {@const renderedColumns = columns.filter((v) => v.id !== '__fixed')}
307
- <table bind:this={csvElement} hidden>
308
- <thead>
309
- <tr>
310
- {#each renderedColumns as column}
311
- <th>{@render column.snippets.title()}</th>
312
- {/each}
313
- </tr>
314
- </thead>
315
- <tbody>
316
- {#each table.data as row, i}
317
- {#if (csv.selected && table.selected.includes(row)) || !csv.selected}
318
- <tr>
319
- {#each renderedColumns as column}
320
- <td>
321
- {#if column.snippets.row}
322
- {@render column.snippets.row(row, {
323
- index: i,
324
- value: column.options.value?.(row),
325
- columnHovered: false,
326
- rowHovered: false,
327
- itemState: {
328
- index: i,
329
- dragging: false,
330
- positioning: false
331
- } as ItemState<any>,
332
- selected: false,
333
- expanded: false,
334
- })}
335
- {:else}
336
- {column.options.value?.(row)}
337
- {/if}
338
- </td>
339
- {/each}
340
- </tr>
341
- {/if}
342
- {/each}
343
- </tbody>
344
- </table>
345
- {/if}
346
-
347
- <svelte:head>
348
- {@html `<style>${style}</style>`}
349
- </svelte:head>
350
-
351
- {#snippet chevronSnippet(rotation: number = 0)}
352
- <svg
353
- xmlns="http://www.w3.org/2000/svg"
354
- width="16"
355
- height="16"
356
- viewBox="0 0 16 16"
357
- style="transform: rotate({rotation}deg)"
358
- >
359
- <path
360
- fill="currentColor"
361
- d="M3.2 10.26a.75.75 0 0 0 1.06.04L8 6.773l3.74 3.527a.75.75 0 1 0 1.02-1.1l-4.25-4a.75.75 0 0 0-1.02 0l-4.25 4a.75.75 0 0 0-.04 1.06"
362
- ></path>
363
- </svg>
364
- {/snippet}
365
-
366
- {#snippet dragSnippet()}
367
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="opacity: .3">
368
- <path
369
- fill="currentColor"
370
- 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"
371
- ></path>
372
- </svg>
373
- {/snippet}
374
-
375
- {#snippet columnsSnippet(
376
- renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
377
- arg: null | ((column: ColumnState) => any[]) = null,
378
- where: 'header' | 'row' | 'statusbar'
379
- )}
380
- {@const isHeader = where === 'header'}
381
- {#each fixed as column, i (column)}
382
- {#if !hidden.includes(column)}
383
- {@const args = arg ? arg(column) : []}
384
- {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
385
- <svelte:element
386
- this={isHeader ? 'th' : 'td'}
387
- class={column.options.class ?? ''}
388
- class:column={true}
389
- class:sticky={true}
390
- class:fixed={true}
391
- use:addRowColumnEvents={[where, column, () => args[1]]}
392
- data-column={column.id}
393
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
394
- class:header={isHeader}
395
- class:sortable
396
- use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
397
- onpointerenter={() => (hoveredColumn = column)}
398
- onpointerleave={() => (hoveredColumn = null)}
399
- >
400
- {@render renderable(column)?.(args[0], args[1])}
401
- {#if isHeader && table.dataState.sortby === column.id && sortable}
402
- <span class='sorting-icon'>
403
- {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
404
- </span>
405
- {/if}
406
- </svelte:element>
407
- {/if}
408
- {/each}
409
- {#each sticky as column, i (column)}
410
- {#if !hidden.includes(column)}
411
- {@const args = arg ? arg(column) : []}
412
- {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
413
- <svelte:element
414
- this={isHeader ? 'th' : 'td'}
415
- class={column.options.class ?? ''}
416
- class:column={true}
417
- class:sticky={true}
418
- use:addRowColumnEvents={[where, column, () => args[1]]}
419
- use:observeColumnWidth={isHeader}
420
- data-column={column.id}
421
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
422
- class:header={isHeader}
423
- class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
424
- class:border={i == sticky.length - 1}
425
- class:sortable
426
- use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
427
- onpointerenter={() => (hoveredColumn = column)}
428
- onpointerleave={() => (hoveredColumn = null)}
429
- >
430
- {@render renderable(column)?.(args[0], args[1])}
431
- {#if isHeader && table.dataState.sortby === column.id && sortable}
432
- <span class='sorting-icon'>
433
- {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
434
- </span>
435
- {/if}
436
- </svelte:element>
437
- {/if}
438
- {/each}
439
- {#each scrolled as column, i (column)}
440
- {#if !hidden.includes(column)}
441
- {@const args = arg ? arg(column) : []}
442
- {@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
443
- <svelte:element
444
- this={isHeader ? 'th' : 'td'}
445
- class={column.options.class ?? ''}
446
- class:column={true}
447
- data-column={column.id}
448
- class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
449
- use:addRowColumnEvents={[where, column, () => args[1]]}
450
- use:observeColumnWidth={isHeader}
451
- class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
452
- class:sortable
453
- use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
454
- onpointerenter={() => (hoveredColumn = column)}
455
- onpointerleave={() => (hoveredColumn = null)}
456
- >
457
- {@render renderable(column)?.(args[0], args[1])}
458
- {#if isHeader && table.dataState.sortby === column.id && sortable}
459
- <span class='sorting-icon'>
460
- {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
461
- </span>
462
- {/if}
463
- </svelte:element>
464
- {/if}
465
- {/each}
466
- {/snippet}
467
-
468
- {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
469
- {ctx.value}
470
- {/snippet}
471
-
472
- {#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
473
- {@const index = itemState?.index ?? 0}
474
-
475
- {@const ctx: RowCtx<T> = {
476
- get index() {
477
- return index
478
- },
479
- get rowHovered() {
480
- return hoveredRow === item
481
- },
482
- get selected() {
483
- return table.selected?.includes(item)
484
- },
485
- set selected(value) {
486
- value ?
487
- table.selected!.push(item)
488
- : table.selected!.splice(table.selected!.indexOf(item), 1)
489
- },
490
- get itemState() {
491
- return itemState
492
- },
493
- get expanded() {
494
- return expandedRow.includes(item)
495
- },
496
- set expanded(value) {
497
- toggleExpand(item, value)
498
- }
499
- }}
500
-
501
- <tr
502
- aria-rowindex={index + 1}
503
- style:opacity={itemState?.positioning ? 0 : 1}
504
- class="row"
505
- class:dragging={itemState?.dragging}
506
- class:selected={table.selected?.includes(item)}
507
- class:first={index === 0}
508
- class:last={index === virtualization.area.length - 1}
509
- {...itemState?.dragging ? { 'data-svelte-tably': table.id } : {}}
510
- onpointerenter={() => (hoveredRow = item)}
511
- onpointerleave={() => (hoveredRow = null)}
512
- use:addRowEvents={ctx}
513
- onclick={(e) => {
514
- if (table.expandable?.options.click === true) {
515
- let target = e.target as HTMLElement
516
- if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
517
- return
518
- }
519
- ctx.expanded = !ctx.expanded
520
- }
521
- }}
522
- >
523
- {@render columnsSnippet(
524
- (column) => column.snippets.row ?? defaultRow,
525
- (column) => {
526
- return [
527
- item,
528
- assignDescriptors(
529
- {
530
- get value() {
531
- return column.options.value ? column.options.value(item) : undefined
532
- },
533
- get columnHovered() {
534
- return hoveredColumn === column
535
- }
536
- },
537
- ctx
538
- )
539
- ]
540
- },
541
- 'row'
542
- )}
543
- {#if table.row?.snippets.context}
544
- {#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
545
- <td
546
- class="context-col"
547
- class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
548
- class:hidden={table.row?.options.context.hover &&
549
- table.row?.snippets.contextHeader &&
550
- hoveredRow !== item}
551
- >
552
- {@render table.row?.snippets.context?.(item, ctx)}
553
- </td>
554
- {/if}
555
- {/if}
556
- </tr>
557
-
558
- {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
559
- min: 1,
560
- duration: table.expandable?.options.slide.duration,
561
- easing: table.expandable?.options.slide.easing
562
- })}
563
- {#if expandableTween.current > 0}
564
- <tr class="expandable" style="height: {expandableTween.current}px">
565
- <td colspan={columns.length} style="height: {expandableTween.current}px">
566
- <div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
567
- {@render table.expandable!.snippets.content?.(item, ctx)}
568
- </div>
569
- </td>
570
- </tr>
571
- {/if}
572
- {/snippet}
573
-
574
- <table
575
- id={table.id}
576
- class="table svelte-tably"
577
- style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
578
- aria-rowcount={table.data.length}
579
- >
580
- {#if columns.some((v) => v.snippets.header)}
581
- <thead class="headers" bind:this={elements.headers}>
582
- <tr style="min-width: {tbody.width}px">
583
- {@render columnsSnippet(
584
- (column) => column.snippets.header,
585
- () => [
586
- {
587
- get header() {
588
- return true
589
- },
590
- get data() {
591
- return table.data
592
- }
593
- }
594
- ],
595
- 'header'
596
- )}
597
- {#if table.row?.snippets.contextHeader}
598
- <th class="context-col">
599
- {@render table.row?.snippets.contextHeader()}
600
- </th>
601
- {/if}
602
- </tr>
603
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
604
- </thead>
605
- {/if}
606
-
607
- <tbody
608
- class="content"
609
- use:reorderArea={{ axis: 'y', class: table.id }}
610
- bind:this={virtualization.viewport.element}
611
- onscrollcapture={onscroll}
612
- bind:clientHeight={virtualization.viewport.height}
613
- bind:clientWidth={tbody.width}
614
- >
615
- {#if table.options.reorderable}
616
- {@render reorderArea({
617
- get view() {
618
- return virtualization.area
619
- },
620
- get modify() {
621
- return table.dataState.origin
622
- },
623
- get startIndex() {
624
- return virtualization.topIndex
625
- }
626
- })}
627
- {:else}
628
- {#each virtualization.area as item, i (item)}
629
- {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
630
- {/each}
631
- {/if}
632
- </tbody>
633
-
634
- {#if columns.some((v) => v.snippets.statusbar)}
635
- <tfoot class="statusbar" bind:this={elements.statusbar}>
636
- <tr>
637
- {@render columnsSnippet(
638
- (column) => column.snippets.statusbar,
639
- () => [
640
- {
641
- get data() {
642
- return table.data
643
- }
644
- }
645
- ],
646
- 'statusbar'
647
- )}
648
- </tr>
649
- <tr style="width:400px;background:none;pointer-events:none;"></tr>
650
- </tfoot>
651
- {/if}
652
-
653
- <caption class="panel" style="width: {panelTween.current}px;">
654
- {#if properties.panel && properties.panel in table.panels}
655
- <div
656
- class="panel-content"
657
- bind:offsetWidth={panelTween.size}
658
- in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
659
- out:fly={{ x: 100, duration: 200, easing: sineInOut }}
660
- >
661
- {@render table.panels[properties.panel].children({
662
- get table() {
663
- return table
664
- },
665
- get data() {
666
- return table.data
667
- }
668
- })}
669
- </div>
670
- {/if}
671
- </caption>
672
- <caption
673
- class="backdrop"
674
- aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}
675
- >
676
- <button
677
- aria-label="Panel backdrop"
678
- class="btn-backdrop"
679
- tabindex="-1"
680
- onclick={() => (properties.panel = undefined)}
681
- ></button>
682
- </caption>
683
- </table>
684
-
685
- {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
686
- <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
687
- {/snippet}
688
-
689
- {#snippet rowSelected(ctx: RowSelectCtx<T>)}
690
- <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
691
- {/snippet}
692
-
693
- {#if table.options.select || table.options.reorderable || table.expandable}
694
- {@const { select, reorderable } = table.options}
695
- {@const expandable = table.expandable}
696
- {@const {
697
- show = 'hover',
698
- style = 'column',
699
- rowSnippet = rowSelected,
700
- headerSnippet = headerSelected
701
- } = typeof select === 'boolean' ? {} : select}
702
- {#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
703
- <Column
704
- id="__fixed"
705
- {table}
706
- fixed
707
- width={Math.max(
708
- 48,
709
- 0 +
710
- (select && show !== 'never' ? 34 : 0) +
711
- (reorderable ? 34 : 0) +
712
- (expandable && expandable?.options.chevron !== 'never' ? 34 : 0)
713
- )}
714
- resizeable={false}
715
- >
716
- {#snippet header()}
717
- <div class="__fixed">
718
- {#if reorderable}
719
- <span style="width: 16px; display: flex; align-items: center;"></span>
720
- {/if}
721
- {#if select}
722
- {@render headerSnippet({
723
- get isSelected() {
724
- return table.data.length === table.selected?.length && table.data.length > 0
725
- },
726
- set isSelected(value) {
727
- if (value) {
728
- table.selected = table.data
729
- } else {
730
- table.selected = []
731
- }
732
- },
733
- get selected() {
734
- return table.selected!
735
- },
736
- get indeterminate() {
737
- return (
738
- (table.selected?.length || 0) > 0 &&
739
- table.data.length !== table.selected?.length
740
- )
741
- }
742
- })}
743
- {/if}
744
- </div>
745
- {/snippet}
746
- {#snippet row(item, row)}
747
- <div class="__fixed">
748
- {#if reorderable && row.itemState}
749
- <span style="width: 16px; display: flex; align-items: center;" use:row.itemState.handle>
750
- {#if (row.rowHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
751
- {@render dragSnippet()}
752
- {/if}
753
- </span>
754
- {/if}
755
- {#if select && (row.selected || show === 'always' || (row.rowHovered && show === 'hover') || row.expanded)}
756
- {@render rowSnippet({
757
- get isSelected() {
758
- return row.selected
759
- },
760
- set isSelected(value) {
761
- row.selected = value
762
- },
763
- get row() {
764
- return row
765
- },
766
- get item() {
767
- return item
768
- },
769
- get data() {
770
- return table.data
771
- }
772
- })}
773
- {/if}
774
- {#if expandable && expandable?.options.chevron !== 'never'}
775
- <button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
776
- {#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
777
- {@render chevronSnippet(row.expanded ? 180 : 90)}
778
- {/if}
779
- </button>
780
- {/if}
781
- </div>
782
- {/snippet}
783
- </Column>
784
- {/if}
785
- {/if}
786
-
787
- {#if table.options.auto}
788
- {#each Object.keys(table.data[0] || {}) as key}
789
- <Column
790
- id={key}
791
- value={(r) => r[key]}
792
- header={capitalize(segmentize(key))}
793
- sort={typeof table.data[0]?.[key] === 'number' ?
794
- (a, b) => a - b
795
- : (a, b) => String(a).localeCompare(String(b))}
796
- />
797
- {/each}
798
- {/if}
799
-
800
- {@render content?.({
801
- Column,
802
- Panel,
803
- Expandable,
804
- Row,
805
- get table() {
806
- return table
807
- }
808
- })}
809
-
810
- <!---------------------------------------------------->
811
- <style>
812
- .svelte-tably *,
813
- .svelte-tably {
814
- box-sizing: border-box;
815
- background-color: inherit;
816
- }
817
-
818
- .context-col {
819
- display: flex;
820
- align-items: center;
821
- justify-content: center;
822
- position: sticky;
823
- right: 0;
824
- height: 100%;
825
- z-index: 3;
826
- padding: 0;
827
-
828
- &.hover {
829
- position: absolute;
830
- }
831
- &.hidden {
832
- pointer-events: none;
833
- user-select: none;
834
- border-left: none;
835
- background: none;
836
- > :global(*) {
837
- opacity: 0;
838
- }
839
- }
840
- }
841
-
842
- :global(:root) {
843
- --tably-color: hsl(0, 0%, 0%);
844
- --tably-bg: hsl(0, 0%, 100%);
845
- --tably-statusbar: hsl(0, 0%, 98%);
846
-
847
- --tably-border: hsl(0, 0%, 90%);
848
- --tably-border-grid: hsl(0, 0%, 98%);
849
-
850
- --tably-padding-x: 1rem;
851
- --tably-padding-y: 0.5rem;
852
-
853
- --tably-radius: 0.25rem;
854
- }
855
-
856
- .svelte-tably {
857
- position: relative;
858
- overflow: visible;
859
- }
860
-
861
- .expandable {
862
- position: relative;
863
-
864
- & > td {
865
- position: sticky;
866
- left: 1px;
867
- > div {
868
- position: absolute;
869
- overflow: auto;
870
- top: -1.5px;
871
- left: 0;
872
- }
873
- }
874
- }
875
-
876
- .expand-row {
877
- display: flex;
878
- justify-content: center;
879
- align-items: center;
880
- padding: 0;
881
- outline: none;
882
- border: none;
883
- cursor: pointer;
884
- background-color: transparent;
885
- color: inherit;
886
- width: 20px;
887
- height: 100%;
888
-
889
- > svg {
890
- transition: transform 0.15s ease;
891
- }
892
- }
893
-
894
- caption {
895
- all: unset;
896
- }
897
-
898
- input[type='checkbox'] {
899
- width: 18px;
900
- height: 18px;
901
- cursor: pointer;
902
- }
903
-
904
- button.btn-backdrop {
905
- outline: none;
906
- border: none;
907
- cursor: pointer;
908
- }
909
-
910
- .sorting-icon {
911
- align-items: center;
912
- justify-items: end;
913
- margin: 0;
914
- margin-left: auto;
915
- > svg {
916
- transition: transform 0.15s ease;
917
- }
918
- }
919
-
920
- th:not(:last-child) .sorting-icon {
921
- margin-right: var(--tably-padding-x);
922
- }
923
-
924
- .__fixed {
925
- display: flex;
926
- align-items: center;
927
- justify-content: center;
928
- gap: 0.25rem;
929
- position: absolute;
930
- top: 0;
931
- left: 0;
932
- right: 0;
933
- bottom: 0;
934
- width: 100%;
935
- }
936
-
937
- tbody::before,
938
- tbody::after,
939
- selects::before,
940
- selects::after {
941
- content: '';
942
- display: grid;
943
- min-height: 100%;
944
- }
945
-
946
- tbody::before,
947
- selects::before {
948
- height: var(--t);
949
- }
950
- tbody::after,
951
- selects::after {
952
- height: var(--b);
953
- }
954
-
955
- .row:global(:is(a)) {
956
- color: inherit;
957
- text-decoration: inherit;
958
- }
959
-
960
- .backdrop {
961
- position: absolute;
962
- left: 0px;
963
- top: 0px;
964
- bottom: 0px;
965
- right: 0px;
966
- background-color: hsla(0, 0%, 0%, 0.3);
967
- z-index: 3;
968
- opacity: 1;
969
- transition: 0.15s ease;
970
- border: none;
971
- outline: none;
972
- cursor: pointer;
973
-
974
- > button {
975
- position: absolute;
976
- left: 0px;
977
- top: 0px;
978
- bottom: 0px;
979
- right: 0px;
980
- }
981
-
982
- &[aria-hidden='true'] {
983
- opacity: 0;
984
- pointer-events: none;
985
- }
986
- }
987
-
988
- .sticky {
989
- position: sticky;
990
- /* right: 100px; */
991
- z-index: 1;
992
- }
993
-
994
- .sticky.border {
995
- border-right: 1px solid var(--tably-border);
996
- }
997
-
998
- .headers > tr > .column {
999
- overflow: hidden;
1000
- padding: var(--tably-padding-y) 0;
1001
- cursor: default;
1002
- user-select: none;
1003
-
1004
- &:last-child {
1005
- border-right: none;
1006
- }
1007
-
1008
- &.sortable {
1009
- cursor: pointer;
1010
- }
1011
-
1012
- &.resizeable {
1013
- resize: horizontal;
1014
- }
1015
- }
1016
-
1017
- .table {
1018
- display: grid;
1019
- height: auto;
1020
- max-height: 100%;
1021
- position: relative;
1022
-
1023
- color: var(--tably-color);
1024
- background-color: var(--tably-bg);
1025
-
1026
- grid-template-areas:
1027
- 'headers panel'
1028
- 'rows panel'
1029
- 'statusbar panel';
1030
-
1031
- grid-template-columns: auto min-content;
1032
- grid-template-rows: auto 1fr auto;
1033
-
1034
- border: 1px solid var(--tably-border);
1035
- border-radius: var(--tably-radius);
1036
- }
1037
-
1038
- .headers {
1039
- display: flex;
1040
- grid-area: headers;
1041
- z-index: 2;
1042
- overflow: hidden;
1043
- }
1044
-
1045
- .headers > tr > .column {
1046
- width: auto !important;
1047
- border-bottom: 1px solid var(--tably-border);
1048
- }
1049
- .headers > tr > .column, .headers > tr > .context-col {
1050
- border-bottom: 1px solid var(--tably-border);
1051
- border-left: 1px solid var(--tably-border-grid);
1052
- }
1053
-
1054
- .content {
1055
- display: grid;
1056
- grid-auto-rows: max-content;
1057
-
1058
- grid-area: rows;
1059
- scrollbar-width: thin;
1060
- overflow: auto;
1061
- }
1062
-
1063
- .statusbar {
1064
- display: flex;
1065
- grid-area: statusbar;
1066
- overflow: hidden;
1067
- background-color: var(--tably-statusbar);
1068
- }
1069
-
1070
- .statusbar > tr > .column {
1071
- border-top: 1px solid var(--tably-border);
1072
- padding: calc(var(--tably-padding-y) / 2) 0;
1073
- }
1074
-
1075
- .headers > tr,
1076
- .row,
1077
- .statusbar > tr {
1078
- position: relative;
1079
- display: grid;
1080
- width: 100%;
1081
- height: 100%;
1082
-
1083
- & > .column {
1084
- display: flex;
1085
- overflow: hidden;
1086
-
1087
- &:not(.pad), &.pad > :global(*:first-child) {
1088
- padding-left: var(--tably-padding-x);
1089
- }
1090
- }
1091
-
1092
- & > *:last-child:not(.context-col) {
1093
- width: 100%;
1094
-
1095
- &:not(.pad), &.pad > :global(*:first-child) {
1096
- padding-right: var(--tably-padding-x);
1097
- }
1098
- }
1099
- }
1100
-
1101
- .row > .column {
1102
- background-color: var(--tably-bg);
1103
- &:not(.pad), &.pad > :global(*:first-child) {
1104
- padding-top: var(--tably-padding-y);
1105
- padding-bottom: var(--tably-padding-y);
1106
- }
1107
- }
1108
-
1109
- :global(#runic-drag .row) {
1110
- border: 1px solid var(--tably-border-grid);
1111
- border-top: 2px solid var(--tably-border-grid);
1112
- }
1113
-
1114
- .row > * {
1115
- border-left: 1px solid var(--tably-border-grid);
1116
- border-bottom: 1px solid var(--tably-border-grid);
1117
- }
1118
-
1119
- .panel {
1120
- position: relative;
1121
- grid-area: panel;
1122
- height: 100%;
1123
- overflow: hidden;
1124
- border-left: 1px solid var(--tably-border);
1125
-
1126
- z-index: 4;
1127
-
1128
- > .panel-content {
1129
- position: absolute;
1130
- top: 0;
1131
- right: 0;
1132
- bottom: 0;
1133
- width: min-content;
1134
- overflow: auto;
1135
- scrollbar-width: thin;
1136
- padding: var(--tably-padding-y) 0;
1137
- }
1138
- }
1139
- </style>
1
+ <!-- @component
2
+
3
+ This is a description, \
4
+ on how to use this.
5
+
6
+ @example
7
+ <Component />
8
+
9
+ -->
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'
17
+ import {
18
+ TableState,
19
+ type HeaderSelectCtx,
20
+ type RowCtx,
21
+ type RowSelectCtx,
22
+ type TableProps
23
+ } from './table-state.svelte.js'
24
+ import Panel from '../panel/Panel.svelte'
25
+ 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'
29
+ import Expandable from '../expandable/Expandable.svelte'
30
+ import { SizeTween } from '../size-tween.svelte.js'
31
+ import { on } from 'svelte/events'
32
+ import Row from '../row/Row.svelte'
33
+
34
+ type T = $$Generic<Record<PropertyKey, unknown>>
35
+
36
+ type ConstructorReturnType<T extends new (...args: any[]) => any> =
37
+ T extends new (...args: any[]) => infer K ? K : never
38
+ type ConstructorParams<T extends new (...args: any[]) => any> =
39
+ T extends new (...args: infer K) => any ? K : never
40
+
41
+ type ContentCtx<T extends Record<PropertyKey, unknown>> = {
42
+ Column: {
43
+ new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
44
+ <V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
45
+ }
46
+ Panel: typeof Panel<T>
47
+ Expandable: typeof Expandable<T>
48
+ Row: typeof Row<T>
49
+ readonly table: TableState<T>
50
+ }
51
+
52
+ type ContentSnippet = Snippet<[context: ContentCtx<T>]>
53
+
54
+ let {
55
+ content,
56
+ selected: _selected = $bindable([]),
57
+ panel: _panel = $bindable(),
58
+ data: _data = $bindable([]),
59
+ ...restProps
60
+ }: TableProps<T> & { content?: ContentSnippet } = $props()
61
+
62
+ const properties = fromProps(restProps, {
63
+ selected: [() => _selected, (v) => (_selected = v)],
64
+ panel: [() => _panel, (v) => (_panel = v)],
65
+ data: [() => _data, (v) => (_data = v)]
66
+ }) as TableProps<T>
67
+
68
+ const mount = mounted()
69
+
70
+ const reorderArea = reorder(rowSnippet)
71
+
72
+ const elements = $state({}) as Record<
73
+ 'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
74
+ HTMLElement
75
+ >
76
+
77
+ const table = new TableState<T>(properties) as TableState<T>
78
+
79
+ const virtualization = new Virtualization(table)
80
+
81
+ const panelTween = new SizeTween(() => !!properties.panel)
82
+
83
+ let hoveredRow: T | null = $state(null)
84
+ let hoveredColumn: ColumnState | null = $state(null)
85
+
86
+ /** Order of columns */
87
+ const fixed = $derived(table.positions.fixed)
88
+ const hidden = $derived(table.positions.hidden)
89
+ const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
90
+ const sticky = $derived(table.positions.sticky.filter(notHidden))
91
+ const scrolled = $derived(table.positions.scroll.filter(notHidden))
92
+ const columns = $derived([...fixed, ...sticky, ...scrolled])
93
+
94
+ /** Width of each column */
95
+ const columnWidths = $state({}) as Record<string, number>
96
+
97
+ const getWidth = (key: string, def: number = 150) =>
98
+ columnWidths[key] || table.columns[key]?.defaults.width || def
99
+
100
+ /** grid-template-columns for widths */
101
+ const style = $derived.by(() => {
102
+ if (!mount.isMounted) return ''
103
+
104
+ const context = table.row?.snippets.context ? table.row?.options.context.width : ''
105
+
106
+ const templateColumns =
107
+ columns
108
+ .map((column, i, arr) => {
109
+ const width = getWidth(column.id)
110
+ if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
111
+ return `${width}px`
112
+ })
113
+ .join(' ') + context
114
+
115
+ const theadTempla3teColumns = `
116
+ #${table.id} > thead > tr,
117
+ #${table.id} > tfoot > tr {
118
+ grid-template-columns: ${templateColumns};
119
+ }
120
+ `
121
+
122
+ const tbodyTemplateColumns = `
123
+ [data-area-class='${table.id}'] tr.row,
124
+ #${table.id} > tbody::after {
125
+ grid-template-columns: ${templateColumns};
126
+ }
127
+ `
128
+
129
+ let sum = 0
130
+ const stickyLeft = [...fixed, ...sticky]
131
+ .map((column, i, arr) => {
132
+ sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
133
+ return `
134
+ #${table.id} .column.sticky[data-column='${column.id}'],
135
+ [data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
136
+ left: ${sum}px;
137
+ }
138
+ `
139
+ })
140
+ .join('')
141
+
142
+ const columnStyling = columns
143
+ .map((column) =>
144
+ !column.options.style ?
145
+ ''
146
+ : `
147
+ [data-area-class='${table.id}'] .column[data-column='${column.id}'] {
148
+ ${column.options.style}
149
+ }
150
+ `
151
+ )
152
+ .join('')
153
+
154
+ return theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
155
+ })
156
+
157
+ function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
158
+ if (!isHeader) return
159
+
160
+ const key = node.getAttribute('data-column')!
161
+ node.style.width = getWidth(key) + 'px'
162
+
163
+ let mouseup = false
164
+
165
+ const observer = new MutationObserver(() => {
166
+ const width = parseFloat(node.style.width)
167
+ if (width === columnWidths[key]) return
168
+ columnWidths[key] = width
169
+ if (!mouseup) {
170
+ mouseup = true
171
+ window.addEventListener(
172
+ 'click',
173
+ (e) => {
174
+ e.preventDefault()
175
+ e.stopPropagation()
176
+ mouseup = false
177
+ },
178
+ { once: true, capture: true }
179
+ )
180
+ }
181
+ })
182
+
183
+ observer.observe(node, { attributes: true })
184
+ return { destroy: () => observer.disconnect() }
185
+ }
186
+
187
+ let tbody = $state({
188
+ width: 0
189
+ })
190
+ async function onscroll() {
191
+ const target = virtualization.viewport.element!
192
+ if (target.scrollTop !== virtualization.scrollTop) {
193
+ virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
194
+ }
195
+
196
+ if (elements.selects) {
197
+ elements.selects.scrollTop = target?.scrollTop
198
+ }
199
+
200
+ if (!elements.headers) return
201
+ elements.headers.scrollLeft = target.scrollLeft
202
+ elements.statusbar.scrollLeft = target.scrollLeft
203
+ }
204
+
205
+ // * --- CSV --- *
206
+ let csv = $state(false) as false | { selected?: boolean }
207
+ let csvElement = $state() as undefined | HTMLTableElement
208
+ interface CSVOptions {
209
+ /** Semi-colons as separator? */
210
+ semicolon?: boolean
211
+ /** Only selected rows */
212
+ selected?: boolean
213
+ }
214
+ export async function toCSV(opts: CSVOptions = {}) {
215
+ csv = { selected: !!opts.selected }
216
+ let resolve: (value: HTMLTableElement) => void
217
+ const promise = new Promise<HTMLTableElement>((r) => (resolve = r))
218
+
219
+ const clean = $effect.root(() => {
220
+ $effect(() => {
221
+ if (csvElement) {
222
+ resolve(csvElement)
223
+ }
224
+ })
225
+ })
226
+
227
+ let table = await promise
228
+ clean()
229
+
230
+ const separator = opts.semicolon ? ';' : ','
231
+ const rows = Array.from(table.rows)
232
+ const csvRows = [] as string[]
233
+
234
+ for (const row of rows) {
235
+ const cells = Array.from(row.cells)
236
+ const csvCells = cells.map((cell) => {
237
+ let text = cell.textContent?.trim() || ''
238
+
239
+ // Escape double quotes and wrap in quotes if needed
240
+ if (text.includes('"')) {
241
+ text = text.replace(/"/g, '""')
242
+ }
243
+ if (text.includes(separator) || text.includes('"') || text.includes('\n')) {
244
+ text = `"${text}"`
245
+ }
246
+
247
+ return text
248
+ })
249
+ csvRows.push(csvCells.join(separator))
250
+ }
251
+
252
+ csv = false
253
+ return csvRows.join('\n')
254
+ }
255
+ // * --- CSV --- *
256
+
257
+ let expandedRow = $state([]) as T[]
258
+ let expandTick = false
259
+ function toggleExpand(item: T, value?: boolean) {
260
+ if (expandTick) return
261
+ expandTick = true
262
+ requestAnimationFrame(() => (expandTick = false))
263
+
264
+ let indexOf = expandedRow.indexOf(item)
265
+ if (value === undefined) {
266
+ value = indexOf === -1
267
+ }
268
+ if (!value) {
269
+ expandedRow.splice(indexOf, 1)
270
+ return
271
+ }
272
+ if (table.expandable?.options.multiple === true) {
273
+ expandedRow.push(item)
274
+ } else {
275
+ expandedRow[0] = item
276
+ }
277
+ }
278
+
279
+ function addRowColumnEvents(
280
+ node: HTMLTableColElement,
281
+ opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
282
+ ) {
283
+ const [where, column, value] = opts
284
+ if (where !== 'row') return
285
+ if (column.options.onclick) {
286
+ $effect(() => on(node, 'click', (e) => column.options.onclick!(e, value())))
287
+ }
288
+ }
289
+
290
+ function addRowEvents(node: HTMLTableRowElement, ctx: RowCtx<T>) {
291
+ if (table.row?.events.onclick) {
292
+ $effect(() => on(node, 'click', (e) => table.row?.events.onclick!(e, ctx)))
293
+ }
294
+ if (table.row?.events.oncontextmenu) {
295
+ $effect(() => on(node, 'contextmenu', (e) => table.row?.events.oncontextmenu!(e, ctx)))
296
+ }
297
+ }
298
+ </script>
299
+
300
+ <!---------------------------------------------------->
301
+
302
+ {#if csv !== false}
303
+ {@const renderedColumns = columns.filter((v) => v.id !== '__fixed')}
304
+ <table bind:this={csvElement} hidden>
305
+ <thead>
306
+ <tr>
307
+ {#each renderedColumns as column}
308
+ <th>{@render column.snippets.title()}</th>
309
+ {/each}
310
+ </tr>
311
+ </thead>
312
+ <tbody>
313
+ {#each table.data as row, i}
314
+ {#if (csv.selected && table.selected.includes(row)) || !csv.selected}
315
+ <tr>
316
+ {#each renderedColumns as column}
317
+ <td>
318
+ {#if column.snippets.row}
319
+ {@render column.snippets.row(row, {
320
+ index: i,
321
+ value: column.options.value?.(row),
322
+ columnHovered: false,
323
+ rowHovered: false,
324
+ itemState: {
325
+ index: i,
326
+ dragging: false,
327
+ positioning: false
328
+ } as ItemState<any>,
329
+ selected: false,
330
+ expanded: false
331
+ })}
332
+ {:else}
333
+ {column.options.value?.(row)}
334
+ {/if}
335
+ </td>
336
+ {/each}
337
+ </tr>
338
+ {/if}
339
+ {/each}
340
+ </tbody>
341
+ </table>
342
+ {/if}
343
+
344
+ <svelte:head>
345
+ {@html `<`+`style>${style}</style>`}
346
+ </svelte:head>
347
+
348
+ {#snippet chevronSnippet(rotation: number = 0)}
349
+ <svg
350
+ xmlns="http://www.w3.org/2000/svg"
351
+ width="16"
352
+ height="16"
353
+ viewBox="0 0 16 16"
354
+ style="transform: rotate({rotation}deg)"
355
+ >
356
+ <path
357
+ fill="currentColor"
358
+ d="M3.2 10.26a.75.75 0 0 0 1.06.04L8 6.773l3.74 3.527a.75.75 0 1 0 1.02-1.1l-4.25-4a.75.75 0 0 0-1.02 0l-4.25 4a.75.75 0 0 0-.04 1.06"
359
+ ></path>
360
+ </svg>
361
+ {/snippet}
362
+
363
+ {#snippet dragSnippet()}
364
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="opacity: .3">
365
+ <path
366
+ fill="currentColor"
367
+ 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"
368
+ ></path>
369
+ </svg>
370
+ {/snippet}
371
+
372
+ {#snippet columnsSnippet(
373
+ renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
374
+ arg: null | ((column: ColumnState) => any[]) = null,
375
+ where: 'header' | 'row' | 'statusbar'
376
+ )}
377
+ {@const isHeader = where === 'header'}
378
+ {#each fixed as column, i (column)}
379
+ {#if !hidden.includes(column)}
380
+ {@const args = arg ? arg(column) : []}
381
+ {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
382
+ <svelte:element
383
+ this={isHeader ? 'th' : 'td'}
384
+ class={column.options.class ?? ''}
385
+ class:column={true}
386
+ class:sticky={true}
387
+ class:fixed={true}
388
+ use:addRowColumnEvents={[where, column, () => args[1]]}
389
+ data-column={column.id}
390
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
391
+ class:header={isHeader}
392
+ class:sortable
393
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
394
+ onpointerenter={() => (hoveredColumn = column)}
395
+ onpointerleave={() => (hoveredColumn = null)}
396
+ >
397
+ {@render renderable(column)?.(args[0], args[1])}
398
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
399
+ <span class="sorting-icon">
400
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
401
+ </span>
402
+ {/if}
403
+ </svelte:element>
404
+ {/if}
405
+ {/each}
406
+ {#each sticky as column, i (column)}
407
+ {#if !hidden.includes(column)}
408
+ {@const args = arg ? arg(column) : []}
409
+ {@const sortable = isHeader && column.options.sort && !table.options.reorderable}
410
+ <svelte:element
411
+ this={isHeader ? 'th' : 'td'}
412
+ class={column.options.class ?? ''}
413
+ class:column={true}
414
+ class:sticky={true}
415
+ use:addRowColumnEvents={[where, column, () => args[1]]}
416
+ use:observeColumnWidth={isHeader}
417
+ data-column={column.id}
418
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
419
+ class:header={isHeader}
420
+ class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
421
+ class:border={i == sticky.length - 1}
422
+ class:sortable
423
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
424
+ onpointerenter={() => (hoveredColumn = column)}
425
+ onpointerleave={() => (hoveredColumn = null)}
426
+ >
427
+ {@render renderable(column)?.(args[0], args[1])}
428
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
429
+ <span class="sorting-icon">
430
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
431
+ </span>
432
+ {/if}
433
+ </svelte:element>
434
+ {/if}
435
+ {/each}
436
+ {#each scrolled as column, i (column)}
437
+ {#if !hidden.includes(column)}
438
+ {@const args = arg ? arg(column) : []}
439
+ {@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
440
+ <svelte:element
441
+ this={isHeader ? 'th' : 'td'}
442
+ class={column.options.class ?? ''}
443
+ class:column={true}
444
+ data-column={column.id}
445
+ class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
446
+ use:addRowColumnEvents={[where, column, () => args[1]]}
447
+ use:observeColumnWidth={isHeader}
448
+ class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
449
+ class:sortable
450
+ use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
451
+ onpointerenter={() => (hoveredColumn = column)}
452
+ onpointerleave={() => (hoveredColumn = null)}
453
+ >
454
+ {@render renderable(column)?.(args[0], args[1])}
455
+ {#if isHeader && table.dataState.sortby === column.id && sortable}
456
+ <span class="sorting-icon">
457
+ {@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
458
+ </span>
459
+ {/if}
460
+ </svelte:element>
461
+ {/if}
462
+ {/each}
463
+ {/snippet}
464
+
465
+ {#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
466
+ {ctx.value}
467
+ {/snippet}
468
+
469
+ {#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
470
+ {@const index = itemState?.index ?? 0}
471
+
472
+ {@const ctx: RowCtx<T> = {
473
+ get index() {
474
+ return index
475
+ },
476
+ get rowHovered() {
477
+ return hoveredRow === item
478
+ },
479
+ get selected() {
480
+ return table.selected?.includes(item)
481
+ },
482
+ set selected(value) {
483
+ value ?
484
+ table.selected!.push(item)
485
+ : table.selected!.splice(table.selected!.indexOf(item), 1)
486
+ },
487
+ get itemState() {
488
+ return itemState
489
+ },
490
+ get expanded() {
491
+ return expandedRow.includes(item)
492
+ },
493
+ set expanded(value) {
494
+ toggleExpand(item, value)
495
+ }
496
+ }}
497
+
498
+ <tr
499
+ aria-rowindex={index + 1}
500
+ style:opacity={itemState?.positioning ? 0 : 1}
501
+ class="row"
502
+ class:dragging={itemState?.dragging}
503
+ class:selected={table.selected?.includes(item)}
504
+ class:first={index === 0}
505
+ class:last={index === virtualization.area.length - 1}
506
+ {...itemState?.dragging ? { 'data-svelte-tably': table.id } : {}}
507
+ onpointerenter={() => (hoveredRow = item)}
508
+ onpointerleave={() => (hoveredRow = null)}
509
+ use:addRowEvents={ctx}
510
+ onclick={(e) => {
511
+ if (table.expandable?.options.click === true) {
512
+ let target = e.target as HTMLElement
513
+ if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
514
+ return
515
+ }
516
+ ctx.expanded = !ctx.expanded
517
+ }
518
+ }}
519
+ >
520
+ {@render columnsSnippet(
521
+ (column) => column.snippets.row ?? defaultRow,
522
+ (column) => {
523
+ return [
524
+ item,
525
+ assignDescriptors(
526
+ {
527
+ get value() {
528
+ return column.options.value ? column.options.value(item) : undefined
529
+ },
530
+ get columnHovered() {
531
+ return hoveredColumn === column
532
+ }
533
+ },
534
+ ctx
535
+ )
536
+ ]
537
+ },
538
+ 'row'
539
+ )}
540
+ {#if table.row?.snippets.context}
541
+ {#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
542
+ <td
543
+ class="context-col"
544
+ class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
545
+ class:hidden={table.row?.options.context.hover &&
546
+ table.row?.snippets.contextHeader &&
547
+ hoveredRow !== item}
548
+ >
549
+ {@render table.row?.snippets.context?.(item, ctx)}
550
+ </td>
551
+ {/if}
552
+ {/if}
553
+ </tr>
554
+
555
+ {@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
556
+ min: 1,
557
+ duration: table.expandable?.options.slide.duration,
558
+ easing: table.expandable?.options.slide.easing
559
+ })}
560
+ {#if expandableTween.current > 0}
561
+ <tr class="expandable" style="height: {expandableTween.current}px">
562
+ <td colspan={columns.length} style="height: {expandableTween.current}px">
563
+ <div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
564
+ {@render table.expandable!.snippets.content?.(item, ctx)}
565
+ </div>
566
+ </td>
567
+ </tr>
568
+ {/if}
569
+ {/snippet}
570
+
571
+ <table
572
+ id={table.id}
573
+ class="table svelte-tably"
574
+ style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
575
+ aria-rowcount={table.data.length}
576
+ >
577
+ {#if columns.some((v) => v.snippets.header)}
578
+ <thead class="headers" bind:this={elements.headers}>
579
+ <tr style="min-width: {tbody.width}px">
580
+ {@render columnsSnippet(
581
+ (column) => column.snippets.header,
582
+ () => [
583
+ {
584
+ get header() {
585
+ return true
586
+ },
587
+ get data() {
588
+ return table.data
589
+ }
590
+ }
591
+ ],
592
+ 'header'
593
+ )}
594
+ {#if table.row?.snippets.contextHeader}
595
+ <th class="context-col">
596
+ {@render table.row?.snippets.contextHeader()}
597
+ </th>
598
+ {/if}
599
+ </tr>
600
+ <tr style="width:400px;background:none;pointer-events:none;"></tr>
601
+ </thead>
602
+ {/if}
603
+
604
+ <tbody
605
+ class="content"
606
+ use:reorderArea={{ axis: 'y', class: table.id }}
607
+ bind:this={virtualization.viewport.element}
608
+ onscrollcapture={onscroll}
609
+ bind:clientHeight={virtualization.viewport.height}
610
+ bind:clientWidth={tbody.width}
611
+ >
612
+ {#if table.options.reorderable}
613
+ {@render reorderArea({
614
+ get view() {
615
+ return virtualization.area
616
+ },
617
+ get modify() {
618
+ return table.dataState.origin
619
+ },
620
+ get startIndex() {
621
+ return virtualization.topIndex
622
+ }
623
+ })}
624
+ {:else}
625
+ {#each virtualization.area as item, i (item)}
626
+ {@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
627
+ {/each}
628
+ {/if}
629
+ </tbody>
630
+
631
+ {#if columns.some((v) => v.snippets.statusbar)}
632
+ <tfoot class="statusbar" bind:this={elements.statusbar}>
633
+ <tr>
634
+ {@render columnsSnippet(
635
+ (column) => column.snippets.statusbar,
636
+ () => [
637
+ {
638
+ get data() {
639
+ return table.data
640
+ }
641
+ }
642
+ ],
643
+ 'statusbar'
644
+ )}
645
+ </tr>
646
+ <tr style="width:400px;background:none;pointer-events:none;"></tr>
647
+ </tfoot>
648
+ {/if}
649
+
650
+ <caption class="panel" style="width: {panelTween.current}px;">
651
+ {#if properties.panel && properties.panel in table.panels}
652
+ <div
653
+ class="panel-content"
654
+ bind:offsetWidth={panelTween.size}
655
+ in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
656
+ out:fly={{ x: 100, duration: 200, easing: sineInOut }}
657
+ >
658
+ {@render table.panels[properties.panel].children({
659
+ get table() {
660
+ return table
661
+ },
662
+ get data() {
663
+ return table.data
664
+ }
665
+ })}
666
+ </div>
667
+ {/if}
668
+ </caption>
669
+ <caption
670
+ class="backdrop"
671
+ aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}
672
+ >
673
+ <button
674
+ aria-label="Panel backdrop"
675
+ class="btn-backdrop"
676
+ tabindex="-1"
677
+ onclick={() => (properties.panel = undefined)}
678
+ ></button>
679
+ </caption>
680
+ </table>
681
+
682
+ {#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
683
+ <input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
684
+ {/snippet}
685
+
686
+ {#snippet rowSelected(ctx: RowSelectCtx<T>)}
687
+ <input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
688
+ {/snippet}
689
+
690
+ {#if table.options.select || table.options.reorderable || table.expandable}
691
+ {@const { select, reorderable } = table.options}
692
+ {@const expandable = table.expandable}
693
+ {@const {
694
+ show = 'hover',
695
+ style = 'column',
696
+ rowSnippet = rowSelected,
697
+ headerSnippet = headerSelected
698
+ } = typeof select === 'boolean' ? {} : select}
699
+ {#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
700
+ <Column
701
+ id="__fixed"
702
+ {table}
703
+ fixed
704
+ width={Math.max(
705
+ 48,
706
+ 0 +
707
+ (select && show !== 'never' ? 34 : 0) +
708
+ (reorderable ? 34 : 0) +
709
+ (expandable && expandable?.options.chevron !== 'never' ? 34 : 0)
710
+ )}
711
+ resizeable={false}
712
+ >
713
+ {#snippet header()}
714
+ <div class="__fixed">
715
+ {#if reorderable}
716
+ <span style="width: 16px; display: flex; align-items: center;"></span>
717
+ {/if}
718
+ {#if select}
719
+ {@render headerSnippet({
720
+ get isSelected() {
721
+ return table.data.length === table.selected?.length && table.data.length > 0
722
+ },
723
+ set isSelected(value) {
724
+ if (value) {
725
+ table.selected = table.data
726
+ } else {
727
+ table.selected = []
728
+ }
729
+ },
730
+ get selected() {
731
+ return table.selected!
732
+ },
733
+ get indeterminate() {
734
+ return (
735
+ (table.selected?.length || 0) > 0 &&
736
+ table.data.length !== table.selected?.length
737
+ )
738
+ }
739
+ })}
740
+ {/if}
741
+ </div>
742
+ {/snippet}
743
+ {#snippet row(item, row)}
744
+ <div class="__fixed">
745
+ {#if reorderable && row.itemState}
746
+ <span style="width: 16px; display: flex; align-items: center;" use:row.itemState.handle>
747
+ {#if (row.rowHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
748
+ {@render dragSnippet()}
749
+ {/if}
750
+ </span>
751
+ {/if}
752
+ {#if select && (row.selected || show === 'always' || (row.rowHovered && show === 'hover') || row.expanded)}
753
+ {@render rowSnippet({
754
+ get isSelected() {
755
+ return row.selected
756
+ },
757
+ set isSelected(value) {
758
+ row.selected = value
759
+ },
760
+ get row() {
761
+ return row
762
+ },
763
+ get item() {
764
+ return item
765
+ },
766
+ get data() {
767
+ return table.data
768
+ }
769
+ })}
770
+ {/if}
771
+ {#if expandable && expandable?.options.chevron !== 'never'}
772
+ <button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
773
+ {#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
774
+ {@render chevronSnippet(row.expanded ? 180 : 90)}
775
+ {/if}
776
+ </button>
777
+ {/if}
778
+ </div>
779
+ {/snippet}
780
+ </Column>
781
+ {/if}
782
+ {/if}
783
+
784
+ {#if table.options.auto}
785
+ {#each Object.keys(table.data[0] || {}) as key}
786
+ <Column
787
+ id={key}
788
+ value={(r) => r[key]}
789
+ header={capitalize(segmentize(key))}
790
+ sort={typeof table.data[0]?.[key] === 'number' ?
791
+ (a, b) => a - b
792
+ : (a, b) => String(a).localeCompare(String(b))}
793
+ />
794
+ {/each}
795
+ {/if}
796
+
797
+ {@render content?.({
798
+ Column,
799
+ Panel,
800
+ Expandable,
801
+ Row,
802
+ get table() {
803
+ return table
804
+ }
805
+ })}
806
+
807
+ <!---------------------------------------------------->
808
+ <style>
809
+ .svelte-tably *,
810
+ .svelte-tably {
811
+ box-sizing: border-box;
812
+ background-color: inherit;
813
+ }
814
+
815
+ .context-col {
816
+ display: flex;
817
+ align-items: center;
818
+ justify-content: center;
819
+ position: sticky;
820
+ right: 0;
821
+ height: 100%;
822
+ z-index: 3;
823
+ padding: 0;
824
+
825
+ &.hover {
826
+ position: absolute;
827
+ }
828
+ &.hidden {
829
+ pointer-events: none;
830
+ user-select: none;
831
+ border-left: none;
832
+ background: none;
833
+ > :global(*) {
834
+ opacity: 0;
835
+ }
836
+ }
837
+ }
838
+
839
+ :global(:root) {
840
+ --tably-color: hsl(0, 0%, 0%);
841
+ --tably-bg: hsl(0, 0%, 100%);
842
+ --tably-statusbar: hsl(0, 0%, 98%);
843
+
844
+ --tably-border: hsl(0, 0%, 90%);
845
+ --tably-border-grid: hsl(0, 0%, 98%);
846
+
847
+ --tably-padding-x: 1rem;
848
+ --tably-padding-y: 0.5rem;
849
+
850
+ --tably-radius: 0.25rem;
851
+ }
852
+
853
+ .svelte-tably {
854
+ position: relative;
855
+ overflow: visible;
856
+ }
857
+
858
+ .expandable {
859
+ position: relative;
860
+
861
+ & > td {
862
+ position: sticky;
863
+ left: 1px;
864
+ > div {
865
+ position: absolute;
866
+ overflow: auto;
867
+ top: -1.5px;
868
+ left: 0;
869
+ }
870
+ }
871
+ }
872
+
873
+ .expand-row {
874
+ display: flex;
875
+ justify-content: center;
876
+ align-items: center;
877
+ padding: 0;
878
+ outline: none;
879
+ border: none;
880
+ cursor: pointer;
881
+ background-color: transparent;
882
+ color: inherit;
883
+ width: 20px;
884
+ height: 100%;
885
+
886
+ > svg {
887
+ transition: transform 0.15s ease;
888
+ }
889
+ }
890
+
891
+ caption {
892
+ all: unset;
893
+ }
894
+
895
+ input[type='checkbox'] {
896
+ width: 18px;
897
+ height: 18px;
898
+ cursor: pointer;
899
+ }
900
+
901
+ button.btn-backdrop {
902
+ outline: none;
903
+ border: none;
904
+ cursor: pointer;
905
+ }
906
+
907
+ .sorting-icon {
908
+ align-items: center;
909
+ justify-items: end;
910
+ margin: 0;
911
+ margin-left: auto;
912
+ > svg {
913
+ transition: transform 0.15s ease;
914
+ }
915
+ }
916
+
917
+ th:not(:last-child) .sorting-icon {
918
+ margin-right: var(--tably-padding-x);
919
+ }
920
+
921
+ .__fixed {
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ gap: 0.25rem;
926
+ position: absolute;
927
+ top: 0;
928
+ left: 0;
929
+ right: 0;
930
+ bottom: 0;
931
+ width: 100%;
932
+ }
933
+
934
+ tbody::before,
935
+ tbody::after,
936
+ selects::before,
937
+ selects::after {
938
+ content: '';
939
+ display: grid;
940
+ min-height: 100%;
941
+ }
942
+
943
+ tbody::before,
944
+ selects::before {
945
+ height: var(--t);
946
+ }
947
+ tbody::after,
948
+ selects::after {
949
+ height: var(--b);
950
+ }
951
+
952
+ .row:global(:is(a)) {
953
+ color: inherit;
954
+ text-decoration: inherit;
955
+ }
956
+
957
+ .backdrop {
958
+ position: absolute;
959
+ left: 0px;
960
+ top: 0px;
961
+ bottom: 0px;
962
+ right: 0px;
963
+ background-color: hsla(0, 0%, 0%, 0.3);
964
+ z-index: 3;
965
+ opacity: 1;
966
+ transition: 0.15s ease;
967
+ border: none;
968
+ outline: none;
969
+ cursor: pointer;
970
+
971
+ > button {
972
+ position: absolute;
973
+ left: 0px;
974
+ top: 0px;
975
+ bottom: 0px;
976
+ right: 0px;
977
+ }
978
+
979
+ &[aria-hidden='true'] {
980
+ opacity: 0;
981
+ pointer-events: none;
982
+ }
983
+ }
984
+
985
+ .sticky {
986
+ position: sticky;
987
+ /* right: 100px; */
988
+ z-index: 1;
989
+ }
990
+
991
+ .sticky.border {
992
+ border-right: 1px solid var(--tably-border);
993
+ }
994
+
995
+ .headers > tr > .column {
996
+ overflow: hidden;
997
+ padding: var(--tably-padding-y) 0;
998
+ cursor: default;
999
+ user-select: none;
1000
+
1001
+ &:last-child {
1002
+ border-right: none;
1003
+ }
1004
+
1005
+ &.sortable {
1006
+ cursor: pointer;
1007
+ }
1008
+
1009
+ &.resizeable {
1010
+ resize: horizontal;
1011
+ }
1012
+ }
1013
+
1014
+ .table {
1015
+ display: grid;
1016
+ height: auto;
1017
+ max-height: 100%;
1018
+ position: relative;
1019
+
1020
+ color: var(--tably-color);
1021
+ background-color: var(--tably-bg);
1022
+
1023
+ grid-template-areas:
1024
+ 'headers panel'
1025
+ 'rows panel'
1026
+ 'statusbar panel';
1027
+
1028
+ grid-template-columns: auto min-content;
1029
+ grid-template-rows: auto 1fr auto;
1030
+
1031
+ border: 1px solid var(--tably-border);
1032
+ border-radius: var(--tably-radius);
1033
+ }
1034
+
1035
+ .headers {
1036
+ display: flex;
1037
+ grid-area: headers;
1038
+ z-index: 2;
1039
+ overflow: hidden;
1040
+ }
1041
+
1042
+ .headers > tr > .column {
1043
+ width: auto !important;
1044
+ border-bottom: 1px solid var(--tably-border);
1045
+ }
1046
+ .headers > tr > .column,
1047
+ .headers > tr > .context-col {
1048
+ border-bottom: 1px solid var(--tably-border);
1049
+ border-left: 1px solid var(--tably-border-grid);
1050
+ }
1051
+
1052
+ .content {
1053
+ display: grid;
1054
+ grid-auto-rows: max-content;
1055
+
1056
+ grid-area: rows;
1057
+ scrollbar-width: thin;
1058
+ overflow: auto;
1059
+ }
1060
+
1061
+ .statusbar {
1062
+ display: flex;
1063
+ grid-area: statusbar;
1064
+ overflow: hidden;
1065
+ background-color: var(--tably-statusbar);
1066
+ }
1067
+
1068
+ .statusbar > tr > .column {
1069
+ border-top: 1px solid var(--tably-border);
1070
+ padding: calc(var(--tably-padding-y) / 2) 0;
1071
+ }
1072
+
1073
+ .headers > tr,
1074
+ .row,
1075
+ .statusbar > tr {
1076
+ position: relative;
1077
+ display: grid;
1078
+ width: 100%;
1079
+ height: 100%;
1080
+
1081
+ & > .column {
1082
+ display: flex;
1083
+ overflow: hidden;
1084
+
1085
+ &:not(.pad),
1086
+ &.pad > :global(*:first-child) {
1087
+ padding-left: var(--tably-padding-x);
1088
+ }
1089
+ }
1090
+
1091
+ & > *:last-child:not(.context-col) {
1092
+ width: 100%;
1093
+
1094
+ &:not(.pad),
1095
+ &.pad > :global(*:first-child) {
1096
+ padding-right: var(--tably-padding-x);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ .row > .column {
1102
+ background-color: var(--tably-bg);
1103
+ &:not(.pad),
1104
+ &.pad > :global(*:first-child) {
1105
+ padding-top: var(--tably-padding-y);
1106
+ padding-bottom: var(--tably-padding-y);
1107
+ }
1108
+ }
1109
+
1110
+ :global(#runic-drag .row) {
1111
+ border: 1px solid var(--tably-border-grid);
1112
+ border-top: 2px solid var(--tably-border-grid);
1113
+ }
1114
+
1115
+ .row > * {
1116
+ border-left: 1px solid var(--tably-border-grid);
1117
+ border-bottom: 1px solid var(--tably-border-grid);
1118
+ }
1119
+
1120
+ .panel {
1121
+ position: relative;
1122
+ grid-area: panel;
1123
+ height: 100%;
1124
+ overflow: hidden;
1125
+ border-left: 1px solid var(--tably-border);
1126
+
1127
+ z-index: 4;
1128
+
1129
+ > .panel-content {
1130
+ position: absolute;
1131
+ top: 0;
1132
+ right: 0;
1133
+ bottom: 0;
1134
+ width: min-content;
1135
+ overflow: auto;
1136
+ scrollbar-width: thin;
1137
+ padding: var(--tably-padding-y) 0;
1138
+ }
1139
+ }
1140
+ </style>