@invopop/popui 0.1.4-beta.2 → 0.1.4-beta.20

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.
@@ -3,13 +3,16 @@
3
3
  import RangeCalendar from './range-calendar/range-calendar.svelte'
4
4
  import { parseDate, type DateValue } from '@internationalized/date'
5
5
  import type { DateRange } from 'bits-ui'
6
- import { Icon } from '@steeze-ui/svelte-icon'
7
- import { Calendar } from '@invopop/ui-icons'
6
+ import { Icon, type IconSource } from '@steeze-ui/svelte-icon'
8
7
  import Transition from 'svelte-transition'
9
8
  import type { DatePickerProps } from './types'
10
9
  import { clickOutside } from './clickOutside'
11
10
  import BaseButton from './BaseButton.svelte'
12
- import { datesFromToday, toCalendarDate } from './helpers'
11
+ import { datesFromToday, toCalendarDate, resolveIcon } from './helpers'
12
+ import { buttonVariants } from './button/button.svelte'
13
+ import { offset, flip, shift } from 'svelte-floating-ui/dom'
14
+ import { createFloatingActions } from 'svelte-floating-ui'
15
+ import { portal } from 'svelte-portal'
13
16
 
14
17
  const {
15
18
  startOfThisWeek,
@@ -109,23 +112,47 @@
109
112
 
110
113
  let {
111
114
  label = 'Date',
112
- position = 'left',
115
+ placement = 'bottom-start',
113
116
  from = '',
114
117
  to = '',
115
- onSelect
118
+ onSelect,
119
+ stackLeft = false,
120
+ stackRight = false,
121
+ icon = undefined,
122
+ iconTheme = 'default'
116
123
  }: DatePickerProps = $props()
117
124
 
125
+ const [floatingRef, floatingContent] = createFloatingActions({
126
+ strategy: 'absolute',
127
+ placement,
128
+ middleware: [offset(8), flip(), shift()]
129
+ })
130
+
131
+ let resolvedIcon: IconSource | undefined = $state()
132
+
133
+ $effect(() => {
134
+ resolveIcon(icon).then((res) => (resolvedIcon = res))
135
+ })
136
+
118
137
  let selectedPeriod = $state('custom')
119
138
  let value = $state<DateRange>({
120
139
  start: undefined,
121
140
  end: undefined
122
141
  })
123
142
  let isOpen = $state(false)
143
+ let isStacked = $derived(stackLeft || stackRight)
144
+ let hasSelectedDates = $derived(value.start !== undefined)
124
145
  let styles = $derived(
125
- clsx({
126
- 'border-border-selected-bold shadow-active': isOpen,
127
- 'border-border-secondary hover:border-border-default-secondary-hover': !isOpen
128
- })
146
+ isStacked
147
+ ? buttonVariants({
148
+ variant: 'ghost',
149
+ stackedLeft: stackLeft,
150
+ stackedRight: stackRight
151
+ })
152
+ : clsx('border backdrop-blur-sm backdrop-filter', {
153
+ 'border-border-selected-bold shadow-active': isOpen,
154
+ 'border-border-default-secondary hover:border-border-default-secondary-hover': !isOpen
155
+ })
129
156
  )
130
157
  let selectedLabel = $state(label)
131
158
 
@@ -137,9 +164,19 @@
137
164
 
138
165
  $effect(() => {
139
166
  if (from) {
167
+ const startDate = parseDate(from)
168
+ const endDate = to ? parseDate(to) : undefined
169
+
140
170
  value = {
141
- start: parseDate(from),
142
- end: to ? parseDate(to) : undefined
171
+ start: startDate,
172
+ end: endDate
173
+ }
174
+
175
+ // Update label directly without calling getLabel() to avoid circular dependency
176
+ if (startDate === endDate) {
177
+ selectedLabel = getDisplayFromValue(startDate)
178
+ } else {
179
+ selectedLabel = `${getDisplayFromValue(startDate)} → ${getDisplayFromValue(endDate)}`
143
180
  }
144
181
  return
145
182
  }
@@ -183,19 +220,34 @@
183
220
  </script>
184
221
 
185
222
  <div>
186
- <div class="relative">
187
- <button
188
- onclick={() => {
189
- isOpen = !isOpen
190
- }}
191
- class="{styles} datepicker-trigger w-full py-1.25 pl-7 pr-2 text-left border border-border-default-secondary rounded-lg text-foreground placeholder-foreground text-base cursor-pointer"
223
+ <button
224
+ use:floatingRef
225
+ onclick={() => {
226
+ isOpen = !isOpen
227
+ }}
228
+ class="{styles} {isStacked
229
+ ? 'h-7 py-1.5'
230
+ : 'py-1.5'} datepicker-trigger flex items-center w-full {resolvedIcon
231
+ ? 'pl-7'
232
+ : 'pl-2'} pr-2 text-left rounded-lg bg-background cursor-pointer relative overflow-hidden"
233
+ >
234
+ {#if resolvedIcon}
235
+ <Icon
236
+ src={resolvedIcon}
237
+ theme={iconTheme}
238
+ class="h-4 w-4 absolute top-1.5 left-2 text-foreground-default-secondary"
239
+ />
240
+ {/if}
241
+ <span
242
+ class="flex-1 text-base truncate {hasSelectedDates
243
+ ? 'text-foreground'
244
+ : 'text-foreground-default-secondary'}"
192
245
  >
193
246
  {selectedLabel}
194
- </button>
195
- <Icon src={Calendar} class="h-4 w-4 absolute top-2 left-2 text-foreground-default-secondary" />
196
- </div>
247
+ </span>
248
+ </button>
197
249
 
198
- <div class="relative">
250
+ {#if isOpen}
199
251
  <Transition
200
252
  show={isOpen}
201
253
  enter="transition ease-out duration-100"
@@ -207,9 +259,9 @@
207
259
  >
208
260
  <!-- @ts-ignore -->
209
261
  <div
210
- class:left-0={position === 'left'}
211
- class:right-0={position === 'right'}
212
- class="bg-background inline-flex flex-col shadow-lg rounded-xl absolute right-0 top-2 z-40 border border-border"
262
+ use:portal
263
+ use:floatingContent
264
+ class="bg-background inline-flex flex-col shadow-lg rounded-xl absolute z-1001 border border-border"
213
265
  use:clickOutside
214
266
  onclick_outside={() => {
215
267
  if (!isOpen) return
@@ -239,5 +291,5 @@
239
291
  </div>
240
292
  </div>
241
293
  </Transition>
242
- </div>
294
+ {/if}
243
295
  </div>
@@ -76,10 +76,9 @@
76
76
  function handleClick(val: AnyProp) {
77
77
  value = val
78
78
 
79
- onSelect?.(value)
80
-
81
79
  if (multiple) return
82
80
 
81
+ onSelect?.(value)
83
82
  selectDropdown?.toggle()
84
83
  }
85
84
 
@@ -90,6 +89,7 @@
90
89
  if (isEqual(value, val)) return
91
90
 
92
91
  value = val
92
+ onSelect?.(value)
93
93
  }
94
94
  </script>
95
95
 
@@ -13,6 +13,9 @@
13
13
  disabled = false,
14
14
  value = $bindable(''),
15
15
  focusOnLoad = false,
16
+ stackLeft = false,
17
+ stackRight = false,
18
+ widthClass = '',
16
19
  oninput,
17
20
  onkeydown,
18
21
  onfocus,
@@ -30,21 +33,30 @@
30
33
  }, 750)
31
34
  }
32
35
 
36
+ let isStacked = $derived(stackLeft || stackRight)
37
+
33
38
  let inputStyles = $derived(
34
39
  clsx(
35
- 'h-8 w-full rounded-lg border px-2 py-1 text-base tracking-tight bg-background-default-default backdrop-blur-[2px] caret-foreground-accent',
40
+ 'px-2 py-1 text-base tracking-tight bg-background-default-default backdrop-blur-[2px] caret-foreground-accent',
36
41
  'placeholder:text-foreground-default-tertiary',
37
42
  'outline-none focus:ring-0',
43
+ widthClass,
38
44
  {
45
+ // Width defaults
46
+ 'w-full': !isStacked && !widthClass,
47
+ // Stacked styles
48
+ 'h-[26px] border-0 rounded-none hover:bg-background-default-secondary focus:bg-background-default-default':
49
+ isStacked,
50
+ 'rounded-l-lg': isStacked && stackLeft && !stackRight,
51
+ 'rounded-r-lg': isStacked && stackRight && !stackLeft,
52
+ // Non-stacked styles
53
+ 'h-8 rounded-lg border': !isStacked,
39
54
  'pointer-events-none bg-background-default-secondary border-border-default-default':
40
- disabled
41
- },
42
- {
43
- 'text-foreground-critical border-border-critical-bold caret-foreground-critical': errorText
44
- },
45
- {
55
+ !isStacked && disabled,
56
+ 'text-foreground-critical border-border-critical-bold caret-foreground-critical':
57
+ !isStacked && errorText,
46
58
  'text-foreground border-border-default-secondary hover:border-border-default-secondary-hover focus:border-border-selected-bold focus:shadow-active':
47
- !errorText && !disabled
59
+ !isStacked && !errorText && !disabled
48
60
  }
49
61
  )
50
62
  )
@@ -14,14 +14,14 @@
14
14
  {/snippet}
15
15
 
16
16
  <TooltipProvider>
17
- <div class="flex flex-col space-y-2 sm:flex-row sm:space-y-0 items-center">
17
+ <div class="flex flex-col space-y-2 sm:flex-row sm:flex-nowrap sm:space-y-0 items-center">
18
18
  {#each mainIcons as icon, i (i)}
19
19
  <Tooltip>
20
20
  <TooltipTrigger class="shrink-0">
21
21
  <div
22
- class="p-1.5 rounded-md border border-border flex items-center space-x-1 bg-background text-icon"
22
+ class="p-1.5 rounded-md border border-border flex items-center space-x-1 bg-background text-icon shrink-0"
23
23
  >
24
- <img src={icon.url} alt={icon.name} class="size-4" />
24
+ <img src={icon.url} alt={icon.name} class="size-4 shrink-0" />
25
25
  </div>
26
26
  </TooltipTrigger>
27
27
  <TooltipContent>{icon.name}</TooltipContent>
@@ -35,7 +35,7 @@
35
35
  {@render separator()}
36
36
  <Tooltip>
37
37
  <TooltipTrigger class="shrink-0">
38
- <div class="flex items-center justify-center text-icon font-medium text-base size-7">
38
+ <div class="flex items-center justify-center text-icon font-medium text-base size-7 shrink-0">
39
39
  +{restIcons.length}
40
40
  </div>
41
41
  </TooltipTrigger>
@@ -11,6 +11,6 @@
11
11
 
12
12
  {#if shouldShow && config?.icon}
13
13
  <div class="flex justify-center">
14
- <Icon src={config.icon} class={config.iconClass ?? 'size-4'} />
14
+ <Icon src={config.icon} class={config.iconClass ?? 'size-4 text-icon'} />
15
15
  </div>
16
16
  {/if}
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import ButtonUuidCopy from '../../ButtonUuidCopy.svelte'
3
+ import type { UuidCellConfig } from '../data-table-types.js'
4
+
5
+ let { value, config }: { value: string; config?: UuidCellConfig } = $props()
6
+ </script>
7
+
8
+ <ButtonUuidCopy
9
+ uuid={value}
10
+ prefixLength={config?.prefixLength}
11
+ suffixLength={config?.suffixLength}
12
+ full={config?.full}
13
+ disabled={config?.disabled}
14
+ oncopied={config?.onCopy}
15
+ />
@@ -0,0 +1,8 @@
1
+ import type { UuidCellConfig } from '../data-table-types.js';
2
+ type $$ComponentProps = {
3
+ value: string;
4
+ config?: UuidCellConfig;
5
+ };
6
+ declare const UuidCell: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type UuidCell = ReturnType<typeof UuidCell>;
8
+ export default UuidCell;
@@ -4,6 +4,8 @@ import BooleanCell from './cells/boolean-cell.svelte';
4
4
  import TagCell from './cells/tag-cell.svelte';
5
5
  import DateCell from './cells/date-cell.svelte';
6
6
  import CurrencyCell from './cells/currency-cell.svelte';
7
+ import UuidCell from './cells/uuid-cell.svelte';
8
+ import { renderSnippet } from './render-helpers.js';
7
9
  export function createColumns(columns) {
8
10
  return columns.map((col) => {
9
11
  const tanstackCol = {
@@ -19,10 +21,17 @@ export function createColumns(columns) {
19
21
  };
20
22
  // Cell renderer
21
23
  if (col.cell) {
22
- // Custom cell renderer
24
+ // Custom cell renderer - can be a Snippet or a function
23
25
  tanstackCol.cell = ({ row }) => {
24
26
  const value = col.accessorKey ? row.original[col.accessorKey] : undefined;
25
- return col.cell(value, row.original);
27
+ // Check if it's a function or a Snippet
28
+ if (typeof col.cell === 'function') {
29
+ return col.cell(value, row.original);
30
+ }
31
+ else {
32
+ // It's a Snippet, render it with the row data
33
+ return renderSnippet(col.cell, row.original);
34
+ }
26
35
  };
27
36
  }
28
37
  else if (col.cellType) {
@@ -40,6 +49,8 @@ export function createColumns(columns) {
40
49
  return renderComponent(DateCell, { value: value, config: col.cellConfig });
41
50
  case 'currency':
42
51
  return renderComponent(CurrencyCell, { value: value, config: col.cellConfig });
52
+ case 'uuid':
53
+ return renderComponent(UuidCell, { value: value, config: col.cellConfig });
43
54
  default:
44
55
  return value;
45
56
  }
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Button from '../button/button.svelte'
3
3
  import InputSelect from '../InputSelect.svelte'
4
+ import InputText from '../InputText.svelte'
4
5
  import { ArrowLeft, ArrowRight, ScrollLeft, ScrollRight } from '@invopop/ui-icons'
5
6
  import { cn } from '../utils.js'
6
7
  import type { DataTablePaginationProps } from './data-table-types.js'
@@ -16,26 +17,37 @@
16
17
  selectedSlot,
17
18
  unselectedSlot,
18
19
  onPageChange,
19
- onPageSizeChange
20
+ onPageSizeChange,
21
+ data,
22
+ rowCount,
23
+ manualPagination
20
24
  }: DataTablePaginationProps<any> = $props()
21
25
 
22
26
  let currentPage = $derived(table.getState().pagination.pageIndex + 1)
23
- let totalPages = $derived(table.getPageCount())
24
- let totalItems = $derived(table.getFilteredRowModel().rows.length)
25
27
  let rowsPerPage = $derived(table.getState().pagination.pageSize)
28
+ let totalItems = $derived.by(() => {
29
+ // Use direct props for reactivity
30
+ if (manualPagination && rowCount !== undefined) {
31
+ return rowCount
32
+ }
33
+ // For client-side pagination, use data length directly
34
+ return data?.length ?? 0
35
+ })
36
+ // Calculate totalPages from reactive values instead of calling table.getPageCount()
37
+ let totalPages = $derived(Math.ceil(totalItems / rowsPerPage) || 1)
26
38
  let hasSelection = $derived(Object.keys(table.getState().rowSelection).length > 0)
27
39
 
28
40
  let pageInputValue = $derived(`${currentPage}`)
29
41
 
30
- function handlePageInput(event: Event) {
31
- const target = event.target as HTMLInputElement
32
- const value = parseInt(target.value)
33
- if (value >= 1 && value <= totalPages) {
34
- table.setPageIndex(value - 1)
35
- onPageChange?.(value)
36
- } else if (target.value === '') {
37
- // Allow empty input temporarily
38
- pageInputValue = ''
42
+ function handlePageInput(value: string) {
43
+ const numValue = parseInt(value)
44
+ if (numValue >= 1 && numValue <= totalPages) {
45
+ if (manualPagination) {
46
+ table.setPagination({ pageIndex: numValue - 1, pageSize: rowsPerPage })
47
+ } else {
48
+ table.setPageIndex(numValue - 1)
49
+ }
50
+ onPageChange?.(numValue)
39
51
  }
40
52
  }
41
53
 
@@ -43,9 +55,14 @@
43
55
  const target = event.target as HTMLInputElement
44
56
  const value = parseInt(target.value)
45
57
  if (isNaN(value) || value < 1) {
46
- pageInputValue = `${currentPage}`
58
+ target.value = `${currentPage}`
47
59
  } else if (value > totalPages) {
48
- table.setPageIndex(totalPages - 1)
60
+ target.value = `${totalPages}`
61
+ if (manualPagination) {
62
+ table.setPagination({ pageIndex: totalPages - 1, pageSize: rowsPerPage })
63
+ } else {
64
+ table.setPageIndex(totalPages - 1)
65
+ }
49
66
  onPageChange?.(totalPages)
50
67
  }
51
68
  }
@@ -71,7 +88,11 @@
71
88
  size="md"
72
89
  icon={ScrollLeft}
73
90
  onclick={() => {
74
- table.setPageIndex(0)
91
+ if (manualPagination) {
92
+ table.setPagination({ pageIndex: 0, pageSize: rowsPerPage })
93
+ } else {
94
+ table.setPageIndex(0)
95
+ }
75
96
  onPageChange?.(1)
76
97
  }}
77
98
  disabled={currentPage === 1}
@@ -84,7 +105,13 @@
84
105
  icon={ArrowLeft}
85
106
  onclick={() => {
86
107
  const newPage = currentPage - 1
87
- table.previousPage()
108
+ if (manualPagination) {
109
+ // For manual pagination, bypass TanStack's navigation and use setPagination directly
110
+ // to avoid clamping issues with stale pageCount
111
+ table.setPagination({ pageIndex: newPage - 1, pageSize: rowsPerPage })
112
+ } else {
113
+ table.previousPage()
114
+ }
88
115
  onPageChange?.(newPage)
89
116
  }}
90
117
  disabled={currentPage === 1}
@@ -93,16 +120,19 @@
93
120
  />
94
121
  </div>
95
122
  <div class="flex items-center gap-1.5">
96
- <input
97
- type="number"
98
- bind:value={pageInputValue}
99
- min="1"
100
- max={totalPages}
101
- oninput={handlePageInput}
102
- onblur={handlePageBlur}
103
- class="w-12 h-8 px-2 py-1 text-base tracking-tight rounded-lg border border-border-default-secondary bg-background-default-default backdrop-blur-[2px] caret-foreground-accent text-foreground outline-none focus:ring-0 hover:border-border-default-secondary-hover focus:border-border-selected-bold focus:shadow-active [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
104
- />
105
- <span class="text-sm text-foreground-default-secondary whitespace-nowrap">
123
+ <div
124
+ class="w-12 [&>div]:gap-0 [&_input]:!h-7 [&_input]:[appearance:textfield] [&_input]:[&::-webkit-outer-spin-button]:appearance-none [&_input]:[&::-webkit-inner-spin-button]:appearance-none"
125
+ >
126
+ <InputText
127
+ bind:value={pageInputValue}
128
+ type="number"
129
+ min="1"
130
+ max={totalPages}
131
+ oninput={handlePageInput}
132
+ onblur={handlePageBlur}
133
+ />
134
+ </div>
135
+ <span class="text-base text-foreground-default-secondary whitespace-nowrap">
106
136
  / {totalPages}
107
137
  </span>
108
138
  </div>
@@ -113,7 +143,13 @@
113
143
  icon={ArrowRight}
114
144
  onclick={() => {
115
145
  const newPage = currentPage + 1
116
- table.nextPage()
146
+ if (manualPagination) {
147
+ // For manual pagination, bypass TanStack's navigation and use setPagination directly
148
+ // to avoid clamping issues with stale pageCount
149
+ table.setPagination({ pageIndex: newPage - 1, pageSize: rowsPerPage })
150
+ } else {
151
+ table.nextPage()
152
+ }
117
153
  onPageChange?.(newPage)
118
154
  }}
119
155
  disabled={currentPage === totalPages}
@@ -125,7 +161,11 @@
125
161
  size="md"
126
162
  icon={ScrollRight}
127
163
  onclick={() => {
128
- table.setPageIndex(totalPages - 1)
164
+ if (manualPagination) {
165
+ table.setPagination({ pageIndex: totalPages - 1, pageSize: rowsPerPage })
166
+ } else {
167
+ table.setPageIndex(totalPages - 1)
168
+ }
129
169
  onPageChange?.(totalPages)
130
170
  }}
131
171
  disabled={currentPage === totalPages}
@@ -135,7 +175,7 @@
135
175
  </div>
136
176
  </div>
137
177
  {#if showRowsPerPage}
138
- <div class="w-[105px]">
178
+ <div class="w-[105px] [&_select]:!h-7 [&_select]:!py-[4px]">
139
179
  <InputSelect
140
180
  value={`${rowsPerPage}`}
141
181
  options={rowsPerPageOptions.map((size) => ({
@@ -145,7 +185,9 @@
145
185
  onchange={(value) => {
146
186
  const size = Number(value)
147
187
  table.setPageSize(size)
188
+ table.setPageIndex(0)
148
189
  onPageSizeChange?.(size)
190
+ onPageChange?.(1)
149
191
  }}
150
192
  placeholder="Rows per page"
151
193
  disablePlaceholder={true}
@@ -155,7 +197,7 @@
155
197
  {/if}
156
198
  </div>
157
199
  {#if totalItems > 0}
158
- <span class="text-sm text-foreground-default-secondary">
200
+ <span class="text-base text-foreground-default-secondary">
159
201
  {formatNumber(totalItems)}
160
202
  {itemsLabel}
161
203
  </span>
@@ -53,6 +53,10 @@ export function createSvelteTable(options) {
53
53
  }
54
54
  updateOptions();
55
55
  $effect.pre(() => {
56
+ // Access data and columns to track them - this reads but doesn't write
57
+ // so it won't cause infinite loops
58
+ void options.data;
59
+ void options.columns;
56
60
  updateOptions();
57
61
  });
58
62
  return table;
@@ -2,7 +2,7 @@ import type { Component, Snippet } from 'svelte';
2
2
  import type { StatusType, AnyProp, TableAction, EmptyStateProps } from '../types.js';
3
3
  import type { IconSource } from '@steeze-ui/svelte-icon';
4
4
  import type { Table } from '@tanstack/table-core';
5
- export type CellType = 'text' | 'boolean' | 'tag' | 'date' | 'currency' | 'custom';
5
+ export type CellType = 'text' | 'boolean' | 'tag' | 'date' | 'currency' | 'uuid' | 'custom';
6
6
  export interface TextCellConfig {
7
7
  className?: string;
8
8
  }
@@ -26,14 +26,21 @@ export interface DateCellConfig {
26
26
  export interface CurrencyCellConfig {
27
27
  className?: string;
28
28
  }
29
- export type CellConfig = TextCellConfig | BooleanCellConfig | TagCellConfig | DateCellConfig | CurrencyCellConfig;
29
+ export interface UuidCellConfig {
30
+ prefixLength?: number;
31
+ suffixLength?: number;
32
+ full?: boolean;
33
+ disabled?: boolean;
34
+ onCopy?: (value: string) => void;
35
+ }
36
+ export type CellConfig = TextCellConfig | BooleanCellConfig | TagCellConfig | DateCellConfig | CurrencyCellConfig | UuidCellConfig;
30
37
  export interface DataTableColumn<TData> {
31
38
  id: string;
32
39
  accessorKey?: keyof TData;
33
40
  header?: string;
34
41
  cellType?: CellType;
35
42
  cellConfig?: CellConfig;
36
- cell?: (value: any, row: TData) => Snippet | Component | string;
43
+ cell?: Snippet<[TData]> | ((value: any, row: TData) => Snippet | Component | string);
37
44
  enableSorting?: boolean;
38
45
  enableHiding?: boolean;
39
46
  enableResizing?: boolean;
@@ -57,8 +64,13 @@ export interface DataTableProps<TData> {
57
64
  filters?: Snippet;
58
65
  paginationSelectedSlot?: Snippet;
59
66
  paginationUnselectedSlot?: Snippet;
67
+ manualPagination?: boolean;
68
+ pageCount?: number;
69
+ rowCount?: number;
60
70
  onPageChange?: (pageIndex: number) => void;
61
71
  onPageSizeChange?: (pageSize: number) => void;
72
+ onSortingChange?: (columnId: string, direction: 'asc' | 'desc') => void;
73
+ getRowClassName?: (row: TData) => string;
62
74
  }
63
75
  export interface DataTablePaginationProps<T> {
64
76
  table: Table<T>;
@@ -72,4 +84,7 @@ export interface DataTablePaginationProps<T> {
72
84
  unselectedSlot?: Snippet;
73
85
  onPageChange?: (pageIndex: number) => void;
74
86
  onPageSizeChange?: (pageSize: number) => void;
87
+ data?: T[];
88
+ rowCount?: number;
89
+ manualPagination?: boolean;
75
90
  }
@@ -46,8 +46,13 @@
46
46
  filters,
47
47
  paginationSelectedSlot,
48
48
  paginationUnselectedSlot,
49
+ manualPagination = false,
50
+ pageCount,
51
+ rowCount,
49
52
  onPageChange,
50
- onPageSizeChange
53
+ onPageSizeChange,
54
+ onSortingChange,
55
+ getRowClassName
51
56
  }: DataTableProps<TData> = $props()
52
57
 
53
58
  const enableSelection = !disableSelection
@@ -71,7 +76,12 @@
71
76
 
72
77
  // Build TanStack columns from config
73
78
  const columns = $derived.by(() =>
74
- buildColumns<TData>(columnConfig, enableSelection, RowActions, getRowActions !== undefined || rowActions.length > 0)
79
+ buildColumns<TData>(
80
+ columnConfig,
81
+ enableSelection,
82
+ RowActions,
83
+ getRowActions !== undefined || rowActions.length > 0
84
+ )
75
85
  )
76
86
 
77
87
  // Calculate initial column sizes based on available width
@@ -96,14 +106,13 @@
96
106
  })
97
107
 
98
108
  const table = setupTable({
99
- get data() {
100
- return data
101
- },
102
- get columns() {
103
- return columns
104
- },
109
+ getData: () => data,
110
+ getColumns: () => columns,
105
111
  enableSelection,
106
112
  enablePagination,
113
+ manualPagination,
114
+ pageCount,
115
+ getRowCount: () => rowCount,
107
116
  getRowSelection: () => rowSelection,
108
117
  getColumnVisibility: () => columnVisibility,
109
118
  getSorting: () => sorting,
@@ -123,18 +132,23 @@
123
132
 
124
133
  {#snippet StickyCellWrapper({
125
134
  children,
126
- align = 'left'
135
+ align = 'left',
136
+ isFirst = false,
137
+ isLast = false
127
138
  }: {
128
139
  children: any
129
140
  align?: 'left' | 'right'
141
+ isFirst?: boolean
142
+ isLast?: boolean
130
143
  })}
131
144
  <div
132
145
  class={cn(
133
- 'h-10 flex items-center relative group-hover/row:bg-background-default-secondary group-data-[state=selected]/row:bg-background-selected',
134
- align === 'right' ? 'justify-end pl-3 pr-6' : 'pl-6 pr-3'
146
+ 'absolute inset-0 flex items-center group-hover/row:bg-background-default-secondary group-data-[state=selected]/row:bg-background-selected px-3',
147
+ align === 'right' ? 'justify-end' : '',
148
+ { 'pl-6': isFirst, 'pr-6': isLast }
135
149
  )}
136
150
  >
137
- <div class="relative z-10">
151
+ <div class="relative z-10 flex items-center">
138
152
  {@render children()}
139
153
  </div>
140
154
  </div>
@@ -181,7 +195,17 @@
181
195
  <BaseTableHeaderOrderBy
182
196
  sortDirection={column.getIsSorted() === 'asc' ? 'asc' : 'desc'}
183
197
  isActive={column.getIsSorted() !== false}
184
- onOrderBy={(direction) => column.toggleSorting(direction === 'desc')}
198
+ onOrderBy={(direction) => {
199
+ column.toggleSorting(direction === 'desc')
200
+ // Reset to first page when sorting changes (same as page size change)
201
+ if (manualPagination) {
202
+ table.setPageIndex(0)
203
+ onPageChange?.(1)
204
+ }
205
+ if (onSortingChange) {
206
+ onSortingChange(column.id, direction)
207
+ }
208
+ }}
185
209
  onHide={() => column.toggleVisibility(false)}
186
210
  />
187
211
  </BaseDropdown>
@@ -189,20 +213,22 @@
189
213
  {/if}
190
214
  {/snippet}
191
215
 
192
- <div class="flex flex-col h-screen">
216
+ <div class="flex flex-col h-full">
193
217
  <DataTableToolbar {table} {filters} />
194
218
  <div class="flex-1 overflow-hidden flex flex-col">
195
- <div bind:this={containerRef} class="relative bg-background flex-1 overflow-auto">
196
- <Table.Root>
219
+ <div bind:this={containerRef} class="relative bg-background flex-1 overflow-auto" style="overscroll-behavior-x: none;">
220
+ <Table.Root class={data.length === 0 ? 'h-full' : 'h-auto'}>
197
221
  <Table.Header>
198
222
  {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
199
223
  <Table.Row class="hover:!bg-transparent border-b border-border">
200
224
  {#each headerGroup.headers as header, index (header.id)}
201
225
  {@const isLastScrollable = index === headerGroup.headers.length - 2}
226
+ {@const isFirstHeader = index === 0}
227
+ {@const isLastHeader = index === headerGroup.headers.length - 1}
202
228
  <Table.Head
203
229
  colspan={header.colSpan}
204
230
  style={getHeaderStyle(header, isLastScrollable)}
205
- class={getHeaderClasses(header, isLastScrollable)}
231
+ class={getHeaderClasses(header, isLastScrollable, isFirstHeader, isLastHeader)}
206
232
  >
207
233
  {#if !header.isPlaceholder}
208
234
  {#if typeof header.column.columnDef.header === 'string'}
@@ -251,24 +277,28 @@
251
277
  {#each table.getRowModel().rows as row (row.id)}
252
278
  <Table.Row
253
279
  data-state={row.getIsSelected() ? 'selected' : undefined}
254
- class="border-b border-border"
280
+ class={cn('border-b border-border', getRowClassName?.(row.original as TData))}
255
281
  onclick={() => onRowClick?.(row.original as TData)}
256
282
  >
257
283
  {#each row.getVisibleCells() as cell, index (cell.id)}
258
- {@const isLastScrollable = index === row.getVisibleCells().length - 2}
259
284
  {@const visibleCells = row.getVisibleCells()}
285
+ {@const isLastScrollable = index === visibleCells.length - 2}
260
286
  {@const firstDataColumnIndex = visibleCells.findIndex(
261
287
  (c) => c.column.id !== 'select' && c.column.id !== 'actions'
262
288
  )}
263
289
  {@const isFirstDataColumn = index === firstDataColumnIndex}
290
+ {@const isFirstCell = index === 0}
291
+ {@const isLastCell = index === visibleCells.length - 1}
264
292
  <Table.Cell
265
293
  style={getCellStyle(cell, isLastScrollable)}
266
- class={getCellClasses(cell, isLastScrollable, isFirstDataColumn)}
294
+ class={getCellClasses(cell, isLastScrollable, isFirstDataColumn, isFirstCell, isLastCell)}
267
295
  >
268
296
  {#if cell.column.id === 'actions'}
269
297
  {@render StickyCellWrapper({
270
298
  align: 'right',
271
- children: CellContent
299
+ children: CellContent,
300
+ isFirst: isFirstCell,
301
+ isLast: isLastCell
272
302
  })}
273
303
  {#snippet CellContent()}
274
304
  <FlexRender
@@ -279,7 +309,9 @@
279
309
  {:else if cell.column.id === 'select'}
280
310
  {@render StickyCellWrapper({
281
311
  align: 'left',
282
- children: CellContent
312
+ children: CellContent,
313
+ isFirst: isFirstCell,
314
+ isLast: isLastCell
283
315
  })}
284
316
  {#snippet CellContent()}
285
317
  <FlexRender
@@ -297,13 +329,15 @@
297
329
  {/each}
298
330
  </Table.Row>
299
331
  {:else}
300
- <Table.Row>
301
- <Table.Cell colspan={columns.length} class="h-48">
302
- <EmptyState
303
- iconSource={emptyState.iconSource}
304
- title={emptyState.title}
305
- description={emptyState.description}
306
- />
332
+ <Table.Row class="hover:!bg-transparent h-full">
333
+ <Table.Cell colspan={columns.length} class="h-full !p-0">
334
+ <div class="flex items-center justify-center h-full w-full">
335
+ <EmptyState
336
+ iconSource={emptyState.iconSource}
337
+ title={emptyState.title}
338
+ description={emptyState.description}
339
+ />
340
+ </div>
307
341
  </Table.Cell>
308
342
  </Table.Row>
309
343
  {/each}
@@ -313,6 +347,9 @@
313
347
  {#if enablePagination}
314
348
  <DataTablePagination
315
349
  {table}
350
+ {data}
351
+ {rowCount}
352
+ {manualPagination}
316
353
  selectedSlot={paginationSelectedSlot}
317
354
  unselectedSlot={paginationUnselectedSlot}
318
355
  {onPageChange}
@@ -11,6 +11,11 @@ export declare function buildColumns<TData>(columnConfig: DataTableColumn<TData>
11
11
  interface TableSetupOptions<TData> {
12
12
  enableSelection: boolean;
13
13
  enablePagination: boolean;
14
+ manualPagination?: boolean;
15
+ pageCount?: number;
16
+ getRowCount?: () => number | undefined;
17
+ getData?: () => TData[];
18
+ getColumns?: () => any[];
14
19
  getRowSelection: () => RowSelectionState;
15
20
  getColumnVisibility: () => VisibilityState;
16
21
  getSorting: () => SortingState;
@@ -29,8 +34,5 @@ interface TableSetupOptions<TData> {
29
34
  /**
30
35
  * Create the TanStack table instance with all configuration
31
36
  */
32
- export declare function setupTable<TData>(options: TableSetupOptions<TData> & {
33
- data?: TData[];
34
- columns?: any[];
35
- }): import("@tanstack/table-core").Table<unknown>;
37
+ export declare function setupTable<TData>(options: TableSetupOptions<TData>): import("@tanstack/table-core").Table<unknown>;
36
38
  export {};
@@ -55,6 +55,12 @@ export function setupTable(options) {
55
55
  return options.getColumnOrder();
56
56
  }
57
57
  },
58
+ get data() {
59
+ return options.getData?.() ?? [];
60
+ },
61
+ get columns() {
62
+ return options.getColumns?.() ?? [];
63
+ },
58
64
  enableRowSelection: options.enableSelection,
59
65
  enableColumnResizing: true,
60
66
  columnResizeMode: 'onChange',
@@ -116,15 +122,28 @@ export function setupTable(options) {
116
122
  }
117
123
  },
118
124
  getCoreRowModel: getCoreRowModel(),
119
- getPaginationRowModel: options.enablePagination ? getPaginationRowModel() : undefined,
120
- getSortedRowModel: getSortedRowModel()
125
+ getPaginationRowModel: options.enablePagination && !options.manualPagination ? getPaginationRowModel() : undefined,
126
+ getSortedRowModel: getSortedRowModel(),
127
+ // Manual pagination configuration
128
+ manualPagination: options.manualPagination
121
129
  };
122
- // Add data and columns as getters if provided
123
- if (options.data !== undefined) {
124
- tableOptions.data = options.data;
125
- }
126
- if (options.columns !== undefined) {
127
- tableOptions.columns = options.columns;
130
+ // Only provide pageCount/rowCount for manual pagination
131
+ // When manualPagination is false, TanStack Table calculates it automatically
132
+ if (options.manualPagination) {
133
+ Object.assign(tableOptions, {
134
+ get pageCount() {
135
+ // Calculate pageCount from rowCount and current pageSize
136
+ const rowCount = options.getRowCount?.();
137
+ if (rowCount !== undefined) {
138
+ const pageSize = options.getPagination().pageSize;
139
+ return Math.ceil(rowCount / pageSize);
140
+ }
141
+ return options.pageCount ?? -1;
142
+ },
143
+ get rowCount() {
144
+ return options.getRowCount?.();
145
+ }
146
+ });
128
147
  }
129
148
  return createSvelteTable(tableOptions);
130
149
  }
@@ -6,7 +6,7 @@ export declare function getHeaderStyle<TData>(header: Header<TData, unknown>, is
6
6
  /**
7
7
  * Calculate CSS classes for table headers
8
8
  */
9
- export declare function getHeaderClasses<TData>(header: Header<TData, unknown>, isLastScrollable: boolean): string;
9
+ export declare function getHeaderClasses<TData>(header: Header<TData, unknown>, isLastScrollable: boolean, isFirstHeader?: boolean, isLastHeader?: boolean): string;
10
10
  /**
11
11
  * Calculate inline styles for table cells
12
12
  */
@@ -14,4 +14,4 @@ export declare function getCellStyle<TData>(cell: Cell<TData, unknown>, isLastSc
14
14
  /**
15
15
  * Calculate CSS classes for table cells
16
16
  */
17
- export declare function getCellClasses<TData>(cell: Cell<TData, unknown>, isLastScrollable: boolean, isFirstDataColumn?: boolean): string;
17
+ export declare function getCellClasses<TData>(cell: Cell<TData, unknown>, isLastScrollable: boolean, isFirstDataColumn?: boolean, isFirstCell?: boolean, isLastCell?: boolean): string;
@@ -15,12 +15,18 @@ export function getHeaderStyle(header, isLastScrollable) {
15
15
  /**
16
16
  * Calculate CSS classes for table headers
17
17
  */
18
- export function getHeaderClasses(header, isLastScrollable) {
18
+ export function getHeaderClasses(header, isLastScrollable, isFirstHeader = false, isLastHeader = false) {
19
+ const isSticky = header.id === 'actions' || header.id === 'select';
19
20
  return clsx('relative whitespace-nowrap overflow-hidden', {
20
- 'sticky right-0 text-right bg-background pl-3 pr-6': header.id === 'actions',
21
- 'sticky left-0 bg-background z-10 pl-6 pr-3': header.id === 'select',
21
+ 'sticky right-0 text-right bg-background': header.id === 'actions',
22
+ 'sticky left-0 bg-background z-10': header.id === 'select',
22
23
  'w-full': isLastScrollable,
23
- 'hover:!bg-transparent': !header.column.getCanSort()
24
+ 'hover:!bg-transparent': !header.column.getCanSort(),
25
+ '!pl-6': isFirstHeader && !isSticky,
26
+ '!pr-6': isLastHeader && !isSticky,
27
+ 'px-3': isSticky,
28
+ 'pl-6': isSticky && isFirstHeader,
29
+ 'pr-6': isSticky && isLastHeader
24
30
  });
25
31
  }
26
32
  /**
@@ -39,11 +45,15 @@ export function getCellStyle(cell, isLastScrollable) {
39
45
  /**
40
46
  * Calculate CSS classes for table cells
41
47
  */
42
- export function getCellClasses(cell, isLastScrollable, isFirstDataColumn = false) {
48
+ export function getCellClasses(cell, isLastScrollable, isFirstDataColumn = false, isFirstCell = false, isLastCell = false) {
49
+ const isSticky = cell.column.id === 'actions' || cell.column.id === 'select';
43
50
  return clsx('whitespace-nowrap overflow-hidden', {
44
- 'sticky right-0 text-right !p-0': cell.column.id === 'actions',
45
- 'sticky left-0 !p-0 z-10': cell.column.id === 'select',
51
+ 'sticky right-0 text-right': cell.column.id === 'actions',
52
+ 'sticky left-0 z-10': cell.column.id === 'select',
53
+ '!p-0': isSticky,
46
54
  'w-full': isLastScrollable,
47
- 'font-medium': isFirstDataColumn
55
+ 'font-medium': isFirstDataColumn,
56
+ '!pl-6': isFirstCell && !isSticky,
57
+ '!pr-6': isLastCell && !isSticky
48
58
  });
49
59
  }
@@ -16,7 +16,7 @@
16
16
  bind:this={ref}
17
17
  data-slot="table-cell"
18
18
  class={cn(
19
- 'py-[9px] [&:has([role=menu])]:py-0 [&:has([data-uuid-copy])]:py-0 pl-3 pr-3 align-middle text-foreground font-normal text-base [&:has([role=menu])]:pl-1 relative z-1 [&:has([role=menu])]:bg-white [&:has([type=checkbox])]:bg-white',
19
+ 'py-[9px] [&:has([role=menu])]:py-0 [&:has([data-uuid-copy])]:py-0 pl-3 pr-3 align-middle text-foreground font-normal text-base [&:has([role=menu])]:pl-1 [&:has([role=menu])]:bg-white [&:has([type=checkbox])]:bg-white',
20
20
  className
21
21
  )}
22
22
  {...restProps}
@@ -13,7 +13,7 @@
13
13
  bind:this={ref}
14
14
  data-slot="table-head"
15
15
  class={cn(
16
- 'text-foreground-default-secondary text-base font-normal text-left align-middle [&:has([role=checkbox])]:pr-0 px-3 hover:bg-background-default-secondary transition-colors',
16
+ 'text-foreground-default-secondary text-base font-normal text-left align-middle [&:has([role=checkbox])]:pr-0 px-3 hover:bg-background-default-secondary',
17
17
  className
18
18
  )}
19
19
  {...restProps}
@@ -19,7 +19,7 @@
19
19
  bind:this={ref}
20
20
  data-slot="table-row"
21
21
  class={cn(
22
- 'group/row data-[state=selected]:bg-background-selected data-[state=checked]:bg-background-selected transition-colors h-10 data-[state=selected]:hover:bg-background-selected data-[state=checked]:hover:bg-background-selected',
22
+ 'group/row data-[state=selected]:bg-background-selected data-[state=checked]:bg-background-selected h-10 data-[state=selected]:hover:bg-background-selected data-[state=checked]:hover:bg-background-selected',
23
23
  className
24
24
  )}
25
25
  {oncontextmenu}
@@ -8,8 +8,8 @@
8
8
  }: WithElementRef<HTMLTableAttributes> = $props()
9
9
  </script>
10
10
 
11
- <div data-slot="table-container" class="relative w-full">
12
- <table bind:this={ref} data-slot="table" class={cn('caption-bottom', className)}>
11
+ <div data-slot="table-container" class="relative w-full h-full">
12
+ <table bind:this={ref} data-slot="table" class={cn('caption-bottom w-full', className)}>
13
13
  {@render children?.()}
14
14
  </table>
15
15
  </div>
package/dist/types.d.ts CHANGED
@@ -368,13 +368,17 @@ export interface DataListItemProps {
368
368
  }
369
369
  export interface DatePickerProps {
370
370
  label?: string;
371
- position?: 'left' | 'right';
371
+ placement?: Placement;
372
372
  from?: string;
373
373
  to?: string;
374
374
  onSelect?: (date: {
375
375
  from: string;
376
376
  to: string;
377
377
  }) => void;
378
+ stackLeft?: boolean;
379
+ stackRight?: boolean;
380
+ icon?: IconSource | string;
381
+ iconTheme?: IconTheme;
378
382
  }
379
383
  export interface DrawerContextProps {
380
384
  items?: DrawerOption[];
@@ -519,6 +523,9 @@ export interface InputTextProps {
519
523
  disabled?: boolean;
520
524
  value?: string | number;
521
525
  focusOnLoad?: boolean;
526
+ stackLeft?: boolean;
527
+ stackRight?: boolean;
528
+ widthClass?: string;
522
529
  oninput?: (value: string) => void;
523
530
  onfocus?: (event: FocusEvent) => void;
524
531
  onblur?: (event: FocusEvent) => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@invopop/popui",
3
3
  "license": "MIT",
4
- "version": "0.1.4-beta.2",
4
+ "version": "0.1.4-beta.20",
5
5
  "repository": {
6
6
  "url": "https://github.com/invopop/popui"
7
7
  },