svelte-tably 1.0.2-next.1 → 1.1.0

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