uibee 3.1.2 → 3.1.4

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,165 +1,191 @@
1
1
  'use client'
2
2
 
3
3
  import { ChevronLeft, ChevronRight } from 'lucide-react'
4
- import { usePathname, useRouter, useSearchParams } from 'next/navigation'
5
- import { useEffect, useState } from 'react'
6
-
7
- type PaginationProps = {
8
- pageSize?: number
9
- totalRows?: number
4
+ import { Suspense, useState } from 'react'
5
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation'
6
+ import type { TableVariant } from './types'
7
+
8
+ export type PaginationProps = {
9
+ totalRows: number
10
+ pageSize: number
11
+ variant?: TableVariant
12
+ urlState?: boolean
13
+ page?: number
14
+ onPageChange?: (page: number) => void
10
15
  }
11
16
 
12
- export default function Pagination({
13
- pageSize = 10,
14
- totalRows,
15
- }: PaginationProps) {
16
- const router = useRouter()
17
- const pathname = usePathname()
18
- const searchParams = useSearchParams()
19
-
20
- const rawPage = parseInt(searchParams.get('page') || '1', 10)
21
- const initialPage = Math.max(1, Number.isNaN(rawPage) ? 1 : rawPage)
22
- const [current, setCurrent] = useState<number>(initialPage)
23
-
24
- const totalPages = Math.max(
25
- 1,
26
- Math.ceil(totalRows !== undefined && pageSize > 0 ? totalRows / pageSize : 1)
27
- )
28
-
29
- useEffect(() => {
30
- const raw = parseInt(searchParams.get('page') || '1', 10)
31
- const p = Math.max(1, Number.isNaN(raw) ? 1 : raw)
32
- setCurrent(Math.max(1, Math.min(totalPages, p)))
33
- }, [searchParams, totalPages])
34
-
35
- function updateQuery(p: number) {
36
- const params = new URLSearchParams(searchParams.toString())
37
- params.set('page', String(p))
38
- router.push(`${pathname}?${params.toString()}`)
39
- }
40
-
41
- function goPrevious() {
42
- if (current <= 1) return
43
- const next = current - 1
44
- setCurrent(next)
45
- updateQuery(next)
46
- }
47
-
48
- function goNext() {
49
- if (current >= totalPages) return
50
- const next = current + 1
51
- setCurrent(next)
52
- updateQuery(next)
53
- }
17
+ type ShellProps = {
18
+ page: number
19
+ totalPages: number
20
+ totalRows: number
21
+ pageSize: number
22
+ onPageChange: (page: number) => void
23
+ variant: TableVariant
24
+ }
54
25
 
55
- function setPage(p: number) {
56
- if (p === current) return
57
- setCurrent(p)
58
- updateQuery(p)
59
- }
26
+ function computeTotalPages(totalRows: number, pageSize: number) {
27
+ return Math.max(1, pageSize > 0 ? Math.ceil(totalRows / pageSize) : 1)
28
+ }
60
29
 
61
- function getPages(curr: number, total: number): (number | string)[] {
62
- const delta = 2
63
- const left = Math.max(1, curr - delta)
64
- const right = Math.min(total, curr + delta)
30
+ function pageRange(current: number, total: number): (number | '…')[] {
31
+ const delta = 2
32
+ const left = Math.max(1, current - delta)
33
+ const right = Math.min(total, current + delta)
34
+ const pages: (number | '…')[] = []
65
35
 
66
- const pages: (number | string)[] = []
36
+ if (left > 1) { pages.push(1); if (left > 2) pages.push('…') }
37
+ for (let i = left; i <= right; i++) pages.push(i)
38
+ if (right < total) { if (right < total - 1) pages.push('…'); pages.push(total) }
67
39
 
68
- if (left > 1) {
69
- pages.push(1)
70
- if (left > 2) pages.push('...')
71
- }
40
+ return pages
41
+ }
72
42
 
73
- for (let i = left; i <= right; i++) pages.push(i)
43
+ function PaginationShell({ page, totalPages, totalRows, pageSize, onPageChange, variant }: ShellProps) {
44
+ const start = totalRows === 0 ? 0 : (page - 1) * pageSize + 1
45
+ const end = Math.min(page * pageSize, totalRows)
46
+ const pages = pageRange(page, totalPages)
74
47
 
75
- if (right < total) {
76
- if (right < total - 1) pages.push('...')
77
- pages.push(total)
78
- }
48
+ const btnBase = `
49
+ flex items-center justify-center rounded-md border text-sm transition-colors duration-100
50
+ disabled:opacity-35 disabled:cursor-not-allowed
51
+ `
79
52
 
80
- return pages
81
- }
53
+ const btnStyle = variant === 'original'
54
+ ? 'border-login-500/50 bg-login-700 hover:bg-login-600 text-login-100'
55
+ : 'border-login-500/40 bg-transparent hover:bg-login-700/60 text-login-100'
82
56
 
83
- const pages = getPages(current, totalPages)
57
+ const pageActive = variant === 'original'
58
+ ? 'border-login bg-login/10 text-login'
59
+ : 'border-login text-login bg-transparent'
84
60
 
85
- const start = Math.max(1, (current - 1) * pageSize + 1)
86
- const end = Math.min(
87
- current * pageSize,
88
- totalRows !== undefined ? totalRows : current * pageSize
89
- )
61
+ const pageInactive = variant === 'original'
62
+ ? 'border-login-500/50 bg-login-700 text-login-100 hover:bg-login-600'
63
+ : 'border-transparent bg-transparent text-login-200 hover:text-login-75 hover:bg-login-700/60'
90
64
 
91
65
  return (
92
- <div className='flex items-center justify-between w-full pt-4'>
93
- <div className='text-sm /70'>
94
- {totalRows !== undefined ? (
95
- totalRows === 0 ? (
96
- <span>Showing 0 results</span>
97
- ) : (
98
- <span>
99
- Showing {start} to {end} of {totalRows} results
100
- </span>
101
- )
102
- ) : null}
103
- </div>
104
-
105
- <div className='flex items-center gap-3'>
66
+ <div className='flex items-center justify-between w-full gap-4 flex-wrap'>
67
+ <span className='text-xs text-login-300 tabular-nums'>
68
+ {totalRows === 0
69
+ ? 'No results'
70
+ : `${start}-${end} of ${totalRows}`
71
+ }
72
+ </span>
73
+
74
+ <nav className='flex items-center gap-1.5' aria-label='Pagination'>
106
75
  <button
107
76
  type='button'
108
- onClick={goPrevious}
109
- disabled={current <= 1}
110
- className={`
111
- flex items-center gap-2 p-1 rounded-lg
112
- bg-login-700 hover:bg-login-600 disabled:opacity-40
113
- border border-login-500/50 text-sm transition-colors duration-150
114
- `}
77
+ aria-label='Previous page'
78
+ disabled={page <= 1}
79
+ onClick={() => onPageChange(page - 1)}
80
+ className={`${btnBase} ${btnStyle} h-8 w-8`}
115
81
  >
116
- <ChevronLeft className='h-5 w-5' />
82
+ <ChevronLeft className='h-4 w-4' />
117
83
  </button>
118
84
 
119
- <nav
120
- className='flex items-center gap-1'
121
- aria-label='Pagination'
122
- >
123
- {pages.map((p, i) =>
124
- typeof p === 'string' ? (
125
- <span key={`e-${i}`} className='px-3 py-1 text-sm'>
126
- {p}
127
- </span>
128
- ) : (
129
- <button
130
- key={p}
131
- type='button'
132
- onClick={() => setPage(p)}
133
- aria-current={
134
- p === current ? 'page' : undefined
135
- }
136
- className={`
137
- px-3 py-1 rounded-lg text-sm border transition-colors duration-150
138
- ${p === current
139
- ? 'bg-login text-white border-login'
140
- : 'bg-login-700 border-login-500/50 hover:bg-login-600'
141
- }
142
- `}
143
- >
144
- {p}
145
- </button>
146
- )
147
- )}
148
- </nav>
85
+ {pages.map((p, i) =>
86
+ p === '…' ? (
87
+ <span key={`ellipsis-${i}`} className='w-8 text-center text-sm text-login-400 select-none'>
88
+
89
+ </span>
90
+ ) : (
91
+ <button
92
+ key={p}
93
+ type='button'
94
+ aria-current={p === page ? 'page' : undefined}
95
+ onClick={() => onPageChange(p)}
96
+ className={`${btnBase} h-8 min-w-8 px-2 tabular-nums ${p === page ? pageActive : pageInactive}`}
97
+ >
98
+ {p}
99
+ </button>
100
+ )
101
+ )}
149
102
 
150
103
  <button
151
104
  type='button'
152
- onClick={goNext}
153
- disabled={current >= totalPages}
154
- className={`
155
- flex items-center gap-2 p-1 rounded-lg select-none
156
- bg-login-700 hover:bg-login-600 disabled:opacity-40
157
- border border-login-500/50 text-sm transition-colors duration-150
158
- `}
105
+ aria-label='Next page'
106
+ disabled={page >= totalPages}
107
+ onClick={() => onPageChange(page + 1)}
108
+ className={`${btnBase} ${btnStyle} h-8 w-8`}
159
109
  >
160
- <ChevronRight className='h-5 w-5' />
110
+ <ChevronRight className='h-4 w-4' />
161
111
  </button>
162
- </div>
112
+ </nav>
163
113
  </div>
164
114
  )
165
115
  }
116
+
117
+ function PaginationLocalState({ totalRows, pageSize, variant = 'original' }: PaginationProps) {
118
+ const [page, setPage] = useState(1)
119
+ return (
120
+ <PaginationShell
121
+ page={page}
122
+ totalPages={computeTotalPages(totalRows, pageSize)}
123
+ totalRows={totalRows}
124
+ pageSize={pageSize}
125
+ onPageChange={setPage}
126
+ variant={variant}
127
+ />
128
+ )
129
+ }
130
+
131
+ function PaginationURLState({ totalRows, pageSize, variant = 'original' }: PaginationProps) {
132
+ const router = useRouter()
133
+ const pathname = usePathname()
134
+ const searchParams = useSearchParams()
135
+
136
+ const page = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10) || 1)
137
+
138
+ function onPageChange(p: number) {
139
+ const params = new URLSearchParams(searchParams.toString())
140
+ params.set('page', String(p))
141
+ router.replace(`${pathname}?${params.toString()}`)
142
+ }
143
+
144
+ return (
145
+ <PaginationShell
146
+ page={page}
147
+ totalPages={computeTotalPages(totalRows, pageSize)}
148
+ totalRows={totalRows}
149
+ pageSize={pageSize}
150
+ onPageChange={onPageChange}
151
+ variant={variant}
152
+ />
153
+ )
154
+ }
155
+
156
+ export default function Pagination(props: PaginationProps) {
157
+ const { totalRows, pageSize, variant = 'original', urlState, page, onPageChange } = props
158
+
159
+ if (urlState) {
160
+ const fallback = (
161
+ <PaginationShell
162
+ page={1}
163
+ totalPages={computeTotalPages(totalRows, pageSize)}
164
+ totalRows={totalRows}
165
+ pageSize={pageSize}
166
+ onPageChange={() => {}}
167
+ variant={variant}
168
+ />
169
+ )
170
+ return (
171
+ <Suspense fallback={fallback}>
172
+ <PaginationURLState {...props} />
173
+ </Suspense>
174
+ )
175
+ }
176
+
177
+ if (page !== undefined && onPageChange !== undefined) {
178
+ return (
179
+ <PaginationShell
180
+ page={page}
181
+ totalPages={computeTotalPages(totalRows, pageSize)}
182
+ totalRows={totalRows}
183
+ pageSize={pageSize}
184
+ onPageChange={onPageChange}
185
+ variant={variant}
186
+ />
187
+ )
188
+ }
189
+
190
+ return <PaginationLocalState {...props} />
191
+ }
@@ -0,0 +1,56 @@
1
+ import type { Density, TableVariant } from './types'
2
+
3
+ type Column = { key: string; width?: string }
4
+ import { DENSITY_TD, DENSITY_TH, VARIANT_TBODY } from './constants'
5
+
6
+ type SkeletonProps = {
7
+ columns: Column[]
8
+ rows: number
9
+ variant: TableVariant
10
+ density: Density
11
+ hasMenu: boolean
12
+ hasSelect: boolean
13
+ }
14
+
15
+ export default function Skeleton({ columns, rows, variant, density, hasMenu, hasSelect }: SkeletonProps) {
16
+ return (
17
+ <tbody className={`block divide-y divide-login-600/15 ${VARIANT_TBODY[variant]}`}>
18
+ {Array.from({ length: rows }).map((_, rowIdx) => (
19
+ <tr key={rowIdx} className='flex w-full'>
20
+ {hasSelect && (
21
+ <td className='flex items-center justify-center'
22
+ style={{ width: '3rem', minWidth: '3rem', flexShrink: 0 }}>
23
+ <span className='block h-4 w-4 rounded animate-shimmer' />
24
+ </td>
25
+ )}
26
+ {columns.map((col, colIdx) => (
27
+ <td
28
+ key={col.key}
29
+ style={col.width ? { width: col.width, flexShrink: 0 } : undefined}
30
+ className={`
31
+ flex-1 min-w-0 flex items-center
32
+ ${DENSITY_TD[density]}
33
+ `}
34
+ >
35
+ <span
36
+ className='block h-4 rounded animate-shimmer'
37
+ style={{
38
+ width: colIdx === 0
39
+ ? `${55 + ((rowIdx * 17 + colIdx * 31) % 30)}%`
40
+ : `${40 + ((rowIdx * 13 + colIdx * 23) % 40)}%`,
41
+ animationDelay: `${(rowIdx * columns.length + colIdx) * 40}ms`,
42
+ }}
43
+ />
44
+ </td>
45
+ ))}
46
+ {hasMenu && (
47
+ <td className='flex items-center justify-end pr-3'
48
+ style={{ width: '3.5rem', minWidth: '3.5rem', flexShrink: 0 }}>
49
+ <span className='block h-5 w-5 rounded animate-shimmer' />
50
+ </td>
51
+ )}
52
+ </tr>
53
+ ))}
54
+ </tbody>
55
+ )
56
+ }