compote-ui 0.33.1 → 0.35.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.
Files changed (39) hide show
  1. package/dist/components/carousel/carousel-item-group.svelte +1 -1
  2. package/dist/components/data-table/column-helper.d.ts +12 -0
  3. package/dist/components/data-table/column-helper.js +36 -0
  4. package/dist/components/data-table/create-table.d.ts +38 -0
  5. package/dist/components/data-table/create-table.js +190 -0
  6. package/dist/components/data-table/data-table-column-filter.svelte +249 -0
  7. package/dist/components/data-table/data-table-column-filter.svelte.d.ts +29 -0
  8. package/dist/components/data-table/data-table-column-visibility.svelte +8 -13
  9. package/dist/components/data-table/data-table-column-visibility.svelte.d.ts +13 -13
  10. package/dist/components/data-table/data-table-title.svelte +2 -2
  11. package/dist/components/data-table/data-table-title.svelte.d.ts +2 -2
  12. package/dist/components/data-table/data-table-toolbar.svelte +28 -5
  13. package/dist/components/data-table/data-table-toolbar.svelte.d.ts +4 -2
  14. package/dist/components/data-table/data-table.svelte +293 -270
  15. package/dist/components/data-table/data-table.svelte.d.ts +19 -16
  16. package/dist/components/data-table/index.d.ts +5 -3
  17. package/dist/components/data-table/index.js +3 -3
  18. package/dist/components/data-table/types.d.ts +50 -0
  19. package/dist/components/data-table/types.js +1 -0
  20. package/dist/components/{data-table → data-table-old}/core/create-table.svelte.js +0 -8
  21. package/dist/components/data-table-old/data-table-column-visibility.svelte +79 -0
  22. package/dist/components/data-table-old/data-table-column-visibility.svelte.d.ts +29 -0
  23. package/dist/components/data-table-old/data-table-title.svelte +16 -0
  24. package/dist/components/data-table-old/data-table-title.svelte.d.ts +10 -0
  25. package/dist/components/data-table-old/data-table-toolbar.svelte +16 -0
  26. package/dist/components/data-table-old/data-table-toolbar.svelte.d.ts +10 -0
  27. package/dist/components/data-table-old/data-table.svelte +342 -0
  28. package/dist/components/data-table-old/data-table.svelte.d.ts +32 -0
  29. package/dist/components/data-table-old/index.d.ts +7 -0
  30. package/dist/components/data-table-old/index.js +7 -0
  31. package/dist/components/scroll-area/scroll-area-viewport.svelte +1 -1
  32. package/package.json +5 -5
  33. /package/dist/components/{data-table → data-table-old}/core/cells.d.ts +0 -0
  34. /package/dist/components/{data-table → data-table-old}/core/cells.js +0 -0
  35. /package/dist/components/{data-table → data-table-old}/core/create-table.svelte.d.ts +0 -0
  36. /package/dist/components/{data-table → data-table-old}/core/index.d.ts +0 -0
  37. /package/dist/components/{data-table → data-table-old}/core/index.js +0 -0
  38. /package/dist/components/{data-table → data-table-old}/data-table-filters.svelte +0 -0
  39. /package/dist/components/{data-table → data-table-old}/data-table-filters.svelte.d.ts +0 -0
@@ -1,126 +1,43 @@
1
- <script lang="ts" generics="TData extends RowData, TSelected = object">
2
- import { useLocaleContext } from '@ark-ui/svelte/locale';
1
+ <script lang="ts" generics="T extends RowData">
3
2
  import { FlexRender } from '@tanstack/svelte-table';
3
+ import type { CellData, Header, RowData } from '@tanstack/svelte-table';
4
+ import type { HTMLAttributes } from 'svelte/elements';
4
5
  import { cn, type ClassValue } from 'tailwind-variants';
5
- import { PhArrowSquareOut, PhCaretDown, PhCaretUp } from '../../icons';
6
+ import { PhArrowSquareOut, PhCaretDown, PhCaretUp, PhCheck, PhX } from '../../icons';
6
7
  import Checkbox from '../checkbox/checkbox.svelte';
7
- import type { HTMLAttributes } from 'svelte/elements';
8
- import type { Header, RowData } from '@tanstack/svelte-table';
9
- import {
10
- getDataTableCellConfig,
11
- hasCustomDataTableCell,
12
- type DataTable,
13
- type DataTableCell,
14
- type DataTableCellConfig,
15
- type DataTableColumnAlign,
16
- type DataTableColumnMeta,
17
- type DataTableFeatures
18
- } from './core';
8
+ import { getColumnId, type DataTableFeatures, type DataTableInstance } from './create-table';
9
+ import type { DataTableColumn, DataTableGroupColumn, DataTableLeafColumn } from './types';
19
10
 
20
11
  type Props = Omit<HTMLAttributes<HTMLDivElement>, 'class'> & {
21
- table: DataTable<TData, TSelected>;
12
+ table: DataTableInstance<T>;
13
+ columns: DataTableColumn<T>[];
14
+ caption?: string;
15
+ emptyMessage?: string;
22
16
  class?: ClassValue;
23
- tableClass?: ClassValue;
24
17
  };
25
18
 
26
- let { table, class: className, tableClass, ...rest }: Props = $props();
27
-
28
- const locale = useLocaleContext();
29
- const columnCount = $derived(table.getVisibleLeafColumns().length);
30
- const headerGroupCount = $derived(table.getHeaderGroups().length);
31
- const isRowSelectionEnabled = $derived(Boolean(table.options.enableRowSelection));
32
- const isMultiRowSelectionEnabled = $derived(table.options.enableMultiRowSelection !== false);
33
- const tableColumnCount = $derived(columnCount + (isRowSelectionEnabled ? 1 : 0));
34
- const allRowsSelected = $derived(table.getIsAllRowsSelected());
35
- const someRowsSelected = $derived(table.getIsSomeRowsSelected());
36
- const allRowsSelectionState = $derived(
37
- allRowsSelected ? true : someRowsSelected ? 'indeterminate' : false
38
- );
39
- const rowSelectionColumnSize = 40;
40
- const tableSize = $derived(
41
- table.getTotalSize() + (isRowSelectionEnabled ? rowSelectionColumnSize : 0)
42
- );
43
- const isColumnResizing = $derived(table.store.state.columnResizing.isResizingColumn !== false);
44
-
45
- function formatNumericValue(
46
- value: unknown,
47
- options: Intl.NumberFormatOptions & { locale?: string }
48
- ) {
49
- if (typeof value !== 'number') return formatTextValue(value);
50
- const { locale: optionLocale, ...numberFormatOptions } = options;
51
- return new Intl.NumberFormat(optionLocale ?? locale().locale, numberFormatOptions).format(
52
- value
53
- );
54
- }
55
-
56
- function formatTextValue(value: unknown, fallback = '') {
57
- if (value === null || value === undefined || value === '') return fallback;
58
- return String(value);
59
- }
60
-
61
- function fractionOptions(options: { fractionDigits?: number }) {
62
- if (options.fractionDigits === undefined) return {};
63
-
64
- return {
65
- minimumFractionDigits: options.fractionDigits,
66
- maximumFractionDigits: options.fractionDigits
67
- };
68
- }
69
-
70
- function isNumericCellConfig(cellConfig: DataTableCellConfig<TData> | undefined) {
71
- return (
72
- cellConfig?.type === 'number' ||
73
- cellConfig?.type === 'currency' ||
74
- cellConfig?.type === 'percentage'
75
- );
76
- }
77
-
78
- function getLinkHref<TValue>(
79
- cell: DataTableCell<TData, TValue>,
80
- href: string | ((value: TValue, row: TData) => string) | undefined
81
- ) {
82
- const value = cell.getValue();
83
- if (!href) return formatTextValue(value);
84
- return typeof href === 'function' ? href(value, cell.row.original) : href;
85
- }
86
-
87
- function getLinkLabel(value: unknown, fallback = 'Open link') {
88
- return formatTextValue(value, fallback);
89
- }
90
-
91
- function getCellMeta<TValue>(cell: DataTableCell<TData, TValue>) {
92
- return (cell.column.columnDef.meta as DataTableColumnMeta<TData, TValue> | undefined)
93
- ?.dataTable;
94
- }
19
+ let {
20
+ table,
21
+ columns,
22
+ caption,
23
+ emptyMessage = 'No rows found',
24
+ class: className,
25
+ ...rest
26
+ }: Props = $props();
95
27
 
96
- function getHeaderMeta(header: Header<DataTableFeatures, TData, unknown>) {
97
- return header.column.columnDef.meta as DataTableColumnMeta<TData> | undefined;
28
+ function alignClass(align: DataTableColumn<T>['align']) {
29
+ return align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
98
30
  }
99
31
 
100
- function getColumnAlign(
101
- align: DataTableColumnAlign | undefined,
102
- cellConfig: DataTableCellConfig<TData> | undefined
103
- ) {
104
- return align ?? (isNumericCellConfig(cellConfig) ? 'right' : 'left');
32
+ function justifyClass(align: DataTableColumn<T>['align']) {
33
+ return align === 'right'
34
+ ? 'justify-end'
35
+ : align === 'center'
36
+ ? 'justify-center'
37
+ : 'justify-start';
105
38
  }
106
39
 
107
- function alignClass(align: DataTableColumnAlign | undefined) {
108
- return {
109
- left: 'text-left',
110
- center: 'text-center',
111
- right: 'text-right'
112
- }[align ?? 'left'];
113
- }
114
-
115
- function justifyClass(align: DataTableColumnAlign | undefined) {
116
- return {
117
- left: 'justify-start',
118
- center: 'justify-center',
119
- right: 'justify-end'
120
- }[align ?? 'left'];
121
- }
122
-
123
- function sortButtonDirectionClass(align: DataTableColumnAlign | undefined) {
40
+ function sortButtonDirectionClass(align: DataTableColumn<T>['align']) {
124
41
  return align === 'right' ? 'flex-row-reverse' : 'flex-row';
125
42
  }
126
43
 
@@ -128,19 +45,30 @@
128
45
  return `width: ${size}px`;
129
46
  }
130
47
 
48
+ function selectionColumnSizeStyle() {
49
+ return 'width: 40px';
50
+ }
51
+
131
52
  function tableSizeStyle() {
132
- return `width: max(100%, ${tableSize}px)`;
53
+ return `width: max(100%, ${table.getTotalSize() + (isRowSelectionEnabled ? 40 : 0)}px)`;
133
54
  }
134
55
 
135
- function resizeHandleStyle(header: Header<DataTableFeatures, TData, unknown>) {
56
+ function resizeHandleStyle(header: Header<DataTableFeatures, T, CellData>) {
136
57
  if (table.options.columnResizeMode !== 'onEnd') return undefined;
137
58
  const deltaOffset = table.store.state.columnResizing.deltaOffset;
138
59
  if (!header.column.getIsResizing() || deltaOffset === null) return undefined;
139
60
  return `transform: translateX(${deltaOffset}px)`;
140
61
  }
141
62
 
63
+ function resizeHandleClass(headerIndex: number, headerCount: number) {
64
+ return cn(
65
+ 'absolute top-0 z-10 flex h-full w-2 cursor-col-resize touch-none items-center justify-center select-none before:h-4 before:w-px before:bg-border before:content-[""]',
66
+ headerIndex === headerCount - 1 ? 'right-0' : '-right-1'
67
+ );
68
+ }
69
+
142
70
  function getHeaderSortDirection(
143
- header: Header<DataTableFeatures, TData, unknown>,
71
+ header: Header<DataTableFeatures, T, CellData>,
144
72
  sortingState: unknown
145
73
  ) {
146
74
  void sortingState;
@@ -158,185 +86,280 @@
158
86
  if (sortDirection === 'desc') return 'descending';
159
87
  return 'none';
160
88
  }
89
+
90
+ function getAllRowsSelectionState(rowSelection: unknown) {
91
+ void rowSelection;
92
+ const allRowsSelected = table.getIsAllRowsSelected();
93
+ const someRowsSelected = table.getIsSomeRowsSelected();
94
+ return allRowsSelected ? true : someRowsSelected ? 'indeterminate' : false;
95
+ }
96
+
97
+ function getRowSelectionState(rowSelection: unknown, rowId: string) {
98
+ void rowSelection;
99
+ return table.getRow(rowId).getIsSelected();
100
+ }
101
+
102
+ function getSelectedRowCount(rowSelection: unknown) {
103
+ void rowSelection;
104
+ return table.getSelectedRowModel().rows.length;
105
+ }
106
+
107
+ function getBooleanCellValue(value: unknown) {
108
+ if (value === true) return true;
109
+ if (value === false) return false;
110
+ return undefined;
111
+ }
112
+
113
+ function getUrlCellValue(value: unknown) {
114
+ if (typeof value !== 'string' || value.trim() === '') return undefined;
115
+ return value;
116
+ }
117
+
118
+ function openUrlCell(value: string) {
119
+ window.open(value, '_blank', 'noopener,noreferrer');
120
+ }
121
+
122
+ function isGroupColumn(column: DataTableColumn<T>): column is DataTableGroupColumn<T> {
123
+ return Array.isArray(column.columns);
124
+ }
125
+
126
+ function isLeafColumn(column: DataTableColumn<T>): column is DataTableLeafColumn<T> {
127
+ return !isGroupColumn(column);
128
+ }
129
+
130
+ function findColumnById(
131
+ columnId: string,
132
+ candidates: DataTableColumn<T>[]
133
+ ): DataTableColumn<T> | undefined {
134
+ for (const column of candidates) {
135
+ if (isGroupColumn(column)) {
136
+ if ((column.id ?? column.header) === columnId) return column;
137
+
138
+ const found = findColumnById(columnId, column.columns);
139
+ if (found) return found;
140
+
141
+ continue;
142
+ }
143
+
144
+ if (isLeafColumn(column) && getColumnId(column) === columnId) return column;
145
+ }
146
+ }
147
+
148
+ const tableStateKey = $derived(JSON.stringify(table.store.state));
149
+ function trackTableState() {
150
+ return tableStateKey;
151
+ }
152
+
153
+ const rowModel = $derived.by(() => {
154
+ trackTableState();
155
+ return table.getRowModel();
156
+ });
157
+ const headerGroups = $derived.by(() => {
158
+ trackTableState();
159
+ return table.getHeaderGroups();
160
+ });
161
+ const visibleLeafColumns = $derived.by(() => {
162
+ trackTableState();
163
+ return table.getVisibleLeafColumns();
164
+ });
165
+ const visibleColumnCount = $derived(visibleLeafColumns.length);
166
+ const isRowSelectionEnabled = $derived(Boolean(table.options.enableRowSelection));
167
+ const isMultiRowSelectionEnabled = $derived(table.options.enableMultiRowSelection !== false);
168
+ const tableColumnCount = $derived(visibleColumnCount + (isRowSelectionEnabled ? 1 : 0));
169
+ const renderedColumnCount = $derived(tableColumnCount + 1);
170
+ const headerGroupCount = $derived(headerGroups.length);
171
+ const allRowsSelectionState = $derived(getAllRowsSelectionState(table.store.state.rowSelection));
172
+ const selectedRowCount = $derived(getSelectedRowCount(table.store.state.rowSelection));
173
+ const isColumnResizing = $derived(table.store.state.columnResizing.isResizingColumn !== false);
161
174
  </script>
162
175
 
163
176
  <div
164
- class={cn('max-h-full min-h-0 overflow-auto rounded-lg border border-surface-3', className)}
177
+ class={cn(
178
+ 'flex max-h-full min-h-0 flex-col overflow-hidden rounded-lg border border-surface-3 bg-surface-1',
179
+ className
180
+ )}
165
181
  {...rest}
166
182
  >
167
183
  {#if isColumnResizing}
168
184
  <div aria-hidden="true" class="fixed inset-0 z-50 cursor-col-resize select-none"></div>
169
185
  {/if}
170
186
 
171
- <table class={cn('table-fixed border-collapse text-sm', tableClass)} style={tableSizeStyle()}>
172
- <colgroup>
173
- {#if isRowSelectionEnabled}
174
- <col style={columnSizeStyle(rowSelectionColumnSize)} />
187
+ <div class="min-h-0 flex-1 overflow-auto">
188
+ <table class="table-fixed border-collapse text-sm" style={tableSizeStyle()}>
189
+ <colgroup>
190
+ {#if isRowSelectionEnabled}
191
+ <col style={selectionColumnSizeStyle()} />
192
+ {/if}
193
+ {#each visibleLeafColumns as column (column.id)}
194
+ <col style={columnSizeStyle(column.getSize())} />
195
+ {/each}
196
+ <col />
197
+ </colgroup>
198
+ {#if caption}
199
+ <caption class="sr-only">{caption}</caption>
175
200
  {/if}
176
- {#each table.getVisibleLeafColumns() as column (column.id)}
177
- <col style={columnSizeStyle(column.getSize())} />
178
- {/each}
179
- </colgroup>
180
- <thead class="sticky top-0 z-20 bg-surface-2 text-left text-ink-dim">
181
- {#each table.getHeaderGroups() as headerGroup, headerGroupIndex (headerGroup.id)}
182
- <tr>
183
- {#if isRowSelectionEnabled && headerGroupIndex === 0}
184
- <th
185
- class="border-b border-surface-3 bg-surface-2 px-3 py-2 text-center align-middle font-medium"
186
- rowspan={headerGroupCount}
187
- >
188
- {#if isMultiRowSelectionEnabled}
201
+ <thead class="sticky top-0 z-20 bg-surface-2 text-left text-ink-dim">
202
+ {#each headerGroups as headerGroup, headerGroupIndex (headerGroup.id)}
203
+ {@const visibleHeaders = headerGroup.headers.filter((header) => header.colSpan > 0)}
204
+ <tr>
205
+ {#if isRowSelectionEnabled && headerGroupIndex === 0}
206
+ <th
207
+ class="border-b border-surface-3 bg-surface-2 px-3 py-2 text-center align-middle font-medium"
208
+ rowspan={headerGroupCount}
209
+ >
210
+ {#if isMultiRowSelectionEnabled}
211
+ <Checkbox
212
+ size="sm"
213
+ aria-label="Select all rows"
214
+ class="mx-auto size-4"
215
+ checked={allRowsSelectionState}
216
+ onCheckedChange={({ checked }) => table.toggleAllRowsSelected(checked === true)}
217
+ />
218
+ {/if}
219
+ </th>
220
+ {/if}
221
+ {#each visibleHeaders as header, headerIndex (header.id)}
222
+ {@const columnDef = findColumnById(header.column.id, columns)}
223
+ {@const sortDirection = getHeaderSortDirection(header, table.store.state.sorting)}
224
+ <th
225
+ class={cn(
226
+ 'relative border-b border-surface-3 bg-surface-2 px-3 py-2 font-medium',
227
+ alignClass(columnDef?.align)
228
+ )}
229
+ colspan={header.colSpan}
230
+ aria-sort={header.column.getCanSort()
231
+ ? getHeaderAriaSort(sortDirection)
232
+ : undefined}
233
+ >
234
+ {#if !header.isPlaceholder}
235
+ {#if header.column.getCanSort()}
236
+ <button
237
+ type="button"
238
+ class={cn(
239
+ 'inline-flex max-w-full items-center gap-1 rounded-sm outline-none hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-offset-2 data-focus-visible:outline-ring',
240
+ justifyClass(columnDef?.align),
241
+ sortButtonDirectionClass(columnDef?.align)
242
+ )}
243
+ aria-label={`${getHeaderSortLabel(sortDirection)}. Toggle sorting.`}
244
+ onclick={header.column.getToggleSortingHandler()}
245
+ >
246
+ <span class="min-w-0 truncate">
247
+ <FlexRender {header} />
248
+ </span>
249
+ <span
250
+ class="inline-flex size-3.5 shrink-0 items-center justify-center text-ink-dim"
251
+ >
252
+ {#if sortDirection === 'asc'}
253
+ <PhCaretUp class="size-3.5" />
254
+ {:else if sortDirection === 'desc'}
255
+ <PhCaretDown class="size-3.5" />
256
+ {/if}
257
+ </span>
258
+ </button>
259
+ {:else}
260
+ <FlexRender {header} />
261
+ {/if}
262
+ {/if}
263
+ {#if header.column.getCanResize()}
264
+ <div
265
+ aria-hidden="true"
266
+ class={resizeHandleClass(headerIndex, visibleHeaders.length)}
267
+ style={resizeHandleStyle(header)}
268
+ ondblclick={() => header.column.resetSize()}
269
+ onmousedown={header.getResizeHandler()}
270
+ ontouchstart={header.getResizeHandler()}
271
+ ></div>
272
+ {/if}
273
+ </th>
274
+ {/each}
275
+ <th aria-hidden="true" class="border-b border-surface-3 bg-surface-2 p-0"></th>
276
+ </tr>
277
+ {/each}
278
+ </thead>
279
+ <tbody>
280
+ {#each rowModel.rows as row (row.id)}
281
+ {@const rowSelected = getRowSelectionState(table.store.state.rowSelection, row.id)}
282
+ <tr
283
+ class={cn(
284
+ 'border-b border-surface-3 last:border-b-0 hover:bg-well/60',
285
+ rowSelected && 'bg-well/60'
286
+ )}
287
+ >
288
+ {#if isRowSelectionEnabled}
289
+ <td class="px-3 py-2 text-center align-middle">
189
290
  <Checkbox
190
291
  size="sm"
191
- aria-label="Select all rows"
292
+ aria-label="Select row"
192
293
  class="mx-auto size-4"
193
- checked={allRowsSelectionState}
194
- onCheckedChange={({ checked }) => table.toggleAllRowsSelected(checked === true)}
294
+ checked={rowSelected}
295
+ disabled={!row.getCanSelect()}
296
+ onCheckedChange={({ checked }) => row.toggleSelected(checked === true)}
195
297
  />
196
- {/if}
197
- </th>
198
- {/if}
199
- {#each headerGroup.headers as header (header.id)}
200
- {@const headerMeta = getHeaderMeta(header)?.dataTable}
201
- {@const headerAlign = getColumnAlign(headerMeta?.align, headerMeta?.cell)}
202
- {@const sortDirection = getHeaderSortDirection(header, table.store.state.sorting)}
203
- <th
204
- class={cn(
205
- 'relative border-b border-surface-3 bg-surface-2 px-3 py-2 font-medium',
206
- alignClass(headerAlign)
207
- )}
208
- colspan={header.colSpan}
209
- aria-sort={header.column.getCanSort() ? getHeaderAriaSort(sortDirection) : undefined}
210
- >
211
- {#if !header.isPlaceholder}
212
- {#if header.column.getCanSort()}
213
- <button
214
- type="button"
215
- class={cn(
216
- 'inline-flex max-w-full items-center gap-1 rounded-sm outline-none hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-offset-2 data-focus-visible:outline-ring',
217
- justifyClass(headerAlign),
218
- sortButtonDirectionClass(headerAlign)
219
- )}
220
- aria-label={`${getHeaderSortLabel(sortDirection)}. Toggle sorting.`}
221
- onclick={header.column.getToggleSortingHandler()}
222
- >
223
- <span class="min-w-0 truncate">
224
- <FlexRender {header} />
298
+ </td>
299
+ {/if}
300
+ {#each row.getVisibleCells() as cell (cell.id)}
301
+ {@const columnDef = findColumnById(cell.column.id, columns)}
302
+ <td class={cn('px-3 py-2 text-ink-dim', alignClass(columnDef?.align))}>
303
+ {#if columnDef?.type === 'boolean'}
304
+ {@const value = getBooleanCellValue(cell.getValue())}
305
+ {#if value === true}
306
+ <span
307
+ class="inline-flex size-5 items-center justify-center text-success"
308
+ role="img"
309
+ aria-label="Yes"
310
+ >
311
+ <PhCheck class="size-4" />
225
312
  </span>
313
+ {:else if value === false}
226
314
  <span
227
- class="inline-flex size-3.5 shrink-0 items-center justify-center text-ink-dim"
315
+ class="inline-flex size-5 items-center justify-center text-danger"
316
+ role="img"
317
+ aria-label="No"
228
318
  >
229
- {#if sortDirection === 'asc'}
230
- <PhCaretUp class="size-3.5" />
231
- {:else if sortDirection === 'desc'}
232
- <PhCaretDown class="size-3.5" />
233
- {/if}
319
+ <PhX class="size-4" />
234
320
  </span>
235
- </button>
236
- {:else}
237
- <FlexRender {header} />
238
- {/if}
239
- {/if}
240
- {#if header.column.getCanResize()}
241
- <div
242
- aria-hidden="true"
243
- class={cn(
244
- 'absolute top-0 -right-1 z-10 flex h-full w-2 cursor-col-resize touch-none items-center justify-center select-none before:h-4 before:w-px before:bg-border before:content-[""]'
245
- )}
246
- style={resizeHandleStyle(header)}
247
- ondblclick={() => header.column.resetSize()}
248
- onmousedown={header.getResizeHandler()}
249
- ontouchstart={header.getResizeHandler()}
250
- ></div>
251
- {/if}
252
- </th>
253
- {/each}
254
- </tr>
255
- {/each}
256
- </thead>
257
- <tbody>
258
- {#each table.getRowModel().rows as row (row.id)}
259
- <tr class="border-b border-surface-3 last:border-b-0">
260
- {#if isRowSelectionEnabled}
261
- <td class="px-3 py-2 text-center align-middle">
262
- <Checkbox
263
- size="sm"
264
- aria-label="Select row"
265
- class="mx-auto size-4"
266
- checked={row.getIsSelected()}
267
- disabled={!row.getCanSelect()}
268
- onCheckedChange={({ checked }) => row.toggleSelected(checked === true)}
269
- />
270
- </td>
271
- {/if}
272
- {#each row.getVisibleCells() as cell (cell.id)}
273
- {@const cellMeta = getCellMeta(cell)}
274
- {@const cellConfig = getDataTableCellConfig(cell)}
275
- {@const cellAlign = getColumnAlign(cellMeta?.align, cellConfig)}
276
- <td
277
- class={cn(
278
- 'px-3 py-2',
279
- alignClass(cellAlign),
280
- isNumericCellConfig(cellConfig) && 'tabular-nums'
281
- )}
282
- >
283
- {#if cellConfig && !hasCustomDataTableCell(cell)}
284
- {@const value = cell.getValue()}
285
- {#if cellConfig.type === 'number'}
286
- {formatNumericValue(value, {
287
- ...cellConfig,
288
- ...fractionOptions(cellConfig)
289
- })}
290
- {:else if cellConfig.type === 'currency'}
291
- {formatNumericValue(value, {
292
- ...cellConfig,
293
- ...fractionOptions(cellConfig),
294
- style: 'currency'
295
- })}
296
- {:else if cellConfig.type === 'percentage'}
297
- {formatNumericValue(value, {
298
- ...cellConfig,
299
- ...fractionOptions(cellConfig),
300
- style: 'percent'
301
- })}
302
- {:else if cellConfig.type === 'boolean'}
303
- {value === true
304
- ? (cellConfig.trueLabel ?? 'Yes')
305
- : value === false
306
- ? (cellConfig.falseLabel ?? 'No')
307
- : (cellConfig.nullLabel ?? '')}
308
- {:else if cellConfig.type === 'link'}
309
- {@const href = getLinkHref(cell, cellConfig.href)}
310
- {#if href}
311
- <a
312
- class="inline-flex size-7 items-center justify-center rounded-md text-primary hover:bg-surface-2"
313
- {href}
314
- target={cellConfig.target ?? '_blank'}
315
- rel="external noreferrer"
316
- aria-label={getLinkLabel(value, cellConfig.fallback)}
317
- title={getLinkLabel(value, cellConfig.fallback)}
321
+ {:else}
322
+ -
323
+ {/if}
324
+ {:else if columnDef?.type === 'url'}
325
+ {@const value = getUrlCellValue(cell.getValue())}
326
+ {#if value}
327
+ <button
328
+ type="button"
329
+ class={cn(
330
+ 'inline-flex max-w-full items-center gap-1.5 rounded-sm font-medium text-ink underline decoration-border decoration-dotted underline-offset-4 outline-none hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
331
+ justifyClass(columnDef.align)
332
+ )}
333
+ onclick={() => openUrlCell(value)}
318
334
  >
319
- <PhArrowSquareOut class="size-4" />
320
- </a>
335
+ <PhArrowSquareOut class="size-3.5 shrink-0" />
336
+ </button>
321
337
  {:else}
322
- {formatTextValue(value, cellConfig.fallback)}
338
+ -
323
339
  {/if}
324
340
  {:else}
325
- {formatTextValue(value, cellConfig.fallback)}
341
+ <FlexRender {cell} />
326
342
  {/if}
327
- {:else}
328
- <FlexRender {cell} />
329
- {/if}
343
+ </td>
344
+ {/each}
345
+ <td aria-hidden="true" class="p-0"></td>
346
+ </tr>
347
+ {:else}
348
+ <tr>
349
+ <td class="px-3 py-10 text-center text-sm text-ink-dim" colspan={renderedColumnCount}>
350
+ {emptyMessage}
330
351
  </td>
331
- {/each}
332
- </tr>
333
- {:else}
334
- <tr>
335
- <td class="px-3 py-6 text-center text-ink-dim" colspan={tableColumnCount}>
336
- No results.
337
- </td>
338
- </tr>
339
- {/each}
340
- </tbody>
341
- </table>
352
+ </tr>
353
+ {/each}
354
+ </tbody>
355
+ </table>
356
+ </div>
357
+
358
+ <div class="shrink-0 border-t border-surface-3 bg-surface-2 px-3 py-2 text-sm text-ink-dim">
359
+ {#if isRowSelectionEnabled}
360
+ {selectedRowCount} of {rowModel.rows.length} rows selected
361
+ {:else}
362
+ {rowModel.rows.length} rows
363
+ {/if}
364
+ </div>
342
365
  </div>