uibee 3.1.1 → 3.1.3

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,44 +1,271 @@
1
1
  'use client'
2
2
 
3
- import Body from './body'
3
+ import { Suspense, useState, useMemo } from 'react'
4
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation'
5
+ import type { TableProps, TableShellProps, TableVariant, Density, SortState, Column } from './types'
4
6
  import Header from './header'
5
- import type { Column } from 'uibee/components'
6
-
7
- type TableProps = {
8
- data: object[]
9
- columns: Column[]
10
- menuItems?: (data: object, id: string) => React.ReactNode
11
- redirectPath?: string | { path: string, key?: string }
12
- variant?: 'default' | 'minimal'
13
- idKey?: string
14
- }
7
+ import Body from './body'
8
+ import Skeleton from './skeleton'
9
+ import Empty from './empty'
10
+ import Pagination from './pagination'
11
+ import { resolveId } from './utils'
12
+ import { VARIANT_CONTAINER } from './constants'
13
+
14
+ function TableShell<T extends Record<string, unknown>>({
15
+ data, columns, idKey, variant, density, striped, loading, loadingRows,
16
+ emptyState, redirectPath, onRowClick, renderExpandedRow,
17
+ selectable, selectedIds, onSelectionChange,
18
+ sort, onSort, page, onPageChange, totalRows, pageSize, hidePagination,
19
+ menuItems, className,
20
+ }: TableShellProps<T>) {
21
+ const allIds = data.map(row =>
22
+ resolveId(row, idKey as string | undefined, columns as Column<Record<string, unknown>>[])
23
+ )
24
+ const allIdsSet = useMemo(() => new Set(allIds), [allIds])
25
+
26
+ const allSelected = allIds.length > 0 && allIds.every(id => selectedIds.includes(id))
27
+ const someSelected = !allSelected && allIds.some(id => selectedIds.includes(id))
15
28
 
16
- export default function Table({ data, columns, menuItems, redirectPath, variant = 'default', idKey }: TableProps) {
17
- if (data.length === 0) {
18
- return <div className='p-4 text-center text-login-200'>No data found</div>
29
+ function handleSelectAll() {
30
+ if (allSelected) {
31
+ onSelectionChange(selectedIds.filter(id => !allIdsSet.has(id)))
32
+ } else {
33
+ onSelectionChange([...new Set([...selectedIds, ...allIds])])
34
+ }
19
35
  }
20
36
 
37
+ const isEmpty = !loading && data.length === 0
38
+ const showPagination = !hidePagination && pageSize > 0 && totalRows > 0
39
+
40
+ const hasMenu = Boolean(menuItems)
41
+ const hasSelect = Boolean(selectable)
42
+ const hasExpand = Boolean(renderExpandedRow)
43
+
44
+ const paginationPad = variant === 'original' ? 'px-4 pb-4 pt-3' : 'pt-4'
45
+
21
46
  return (
22
- <div className={`
23
- flex-1 flex flex-col min-h-0 overflow-x-auto overflow-y-hidden h-full w-full
24
- ${variant === 'default' ? 'bg-login-800 rounded-lg border border-login-600/30' : ''}
25
- ${variant === 'minimal' ? 'bg-transparent' : ''}
26
- `}>
27
- <table className='min-w-full w-max divide-y divide-login-600/30 flex flex-col flex-1 min-h-0'>
28
- <Header
29
- columns={columns}
30
- hideMenu={!menuItems}
31
- variant={variant}
32
- />
33
- <Body
34
- list={data}
35
- columns={columns}
36
- menuItems={menuItems}
37
- redirectPath={redirectPath}
38
- variant={variant}
39
- idKey={idKey}
40
- />
41
- </table>
47
+ <div className={['flex flex-col min-h-0 w-full', VARIANT_CONTAINER[variant], className].filter(Boolean).join(' ')}>
48
+ <div className='flex-1 min-h-0 overflow-x-auto'>
49
+ <table className='min-w-full w-max flex flex-col flex-1 min-h-0 divide-y divide-login-600/20'>
50
+ <Header
51
+ columns={columns}
52
+ sort={sort}
53
+ onSort={onSort}
54
+ hasMenu={hasMenu}
55
+ hasSelect={hasSelect}
56
+ hasExpand={hasExpand}
57
+ allSelected={allSelected}
58
+ someSelected={someSelected}
59
+ onSelectAll={handleSelectAll}
60
+ variant={variant}
61
+ density={density}
62
+ />
63
+ {loading ? (
64
+ <Skeleton
65
+ columns={columns}
66
+ rows={loadingRows}
67
+ variant={variant}
68
+ density={density}
69
+ hasMenu={hasMenu}
70
+ hasSelect={hasSelect}
71
+ />
72
+ ) : isEmpty ? (
73
+ <Empty emptyState={emptyState} />
74
+ ) : (
75
+ <Body
76
+ data={data}
77
+ columns={columns}
78
+ idKey={idKey}
79
+ variant={variant}
80
+ density={density}
81
+ striped={striped}
82
+ redirectPath={redirectPath}
83
+ onRowClick={onRowClick}
84
+ renderExpandedRow={renderExpandedRow}
85
+ selectable={selectable}
86
+ selectedIds={selectedIds}
87
+ onSelectionChange={onSelectionChange}
88
+ menuItems={menuItems}
89
+ />
90
+ )}
91
+ </table>
92
+ </div>
93
+
94
+ {showPagination && (
95
+ <div className={paginationPad}>
96
+ <Pagination
97
+ page={page}
98
+ totalRows={totalRows}
99
+ pageSize={pageSize}
100
+ onPageChange={onPageChange}
101
+ variant={variant}
102
+ />
103
+ </div>
104
+ )}
42
105
  </div>
43
106
  )
44
107
  }
108
+
109
+ // useInternalSelection: falls back to internal state when caller omits selectedIds or onSelectionChange.
110
+ function useInternalSelection<T extends Record<string, unknown>>(
111
+ props: Pick<TableProps<T>, 'selectedIds' | 'onSelectionChange'>
112
+ ) {
113
+ const [internal, setInternal] = useState<string[]>([])
114
+ return {
115
+ selectedIds: props.selectedIds ?? internal,
116
+ onSelectionChange: props.onSelectionChange ?? setInternal,
117
+ }
118
+ }
119
+
120
+ function applyDefaults<T extends Record<string, unknown>>(props: TableProps<T>): {
121
+ variant: TableVariant
122
+ density: Density
123
+ selectable: boolean
124
+ loading: boolean
125
+ loadingRows: number
126
+ striped: boolean
127
+ hidePagination: boolean
128
+ pageSize: number
129
+ } {
130
+ return {
131
+ variant: props.variant ?? 'original',
132
+ density: props.density ?? 'comfortable',
133
+ selectable: props.selectable ?? false,
134
+ loading: props.loading ?? false,
135
+ loadingRows: props.loadingRows ?? (props.pageSize ?? 5),
136
+ striped: props.striped ?? false,
137
+ hidePagination: props.hidePagination ?? false,
138
+ pageSize: props.pageSize ?? 0,
139
+ }
140
+ }
141
+
142
+ function TableLocalState<T extends Record<string, unknown>>(props: TableProps<T>) {
143
+ const defaults = applyDefaults(props)
144
+ const selection = useInternalSelection(props)
145
+ const [sort, setSort] = useState<SortState | undefined>(undefined)
146
+ const [page, setPage] = useState(1)
147
+
148
+ const ps = defaults.pageSize
149
+ const allRows = props.data.length
150
+
151
+ const sorted = useMemo(() => {
152
+ if (!sort) return props.data
153
+ return [...props.data].sort((a, b) => {
154
+ const av = a[sort.column]
155
+ const bv = b[sort.column]
156
+ const cmp = String(av ?? '').localeCompare(String(bv ?? ''), undefined, { numeric: true, sensitivity: 'base' })
157
+ return sort.order === 'asc' ? cmp : -cmp
158
+ })
159
+ }, [props.data, sort])
160
+
161
+ const displayData = ps > 0 ? sorted.slice((page - 1) * ps, page * ps) : sorted
162
+
163
+ function handleSort(newSort: SortState) {
164
+ setSort(newSort)
165
+ setPage(1)
166
+ }
167
+
168
+ return (
169
+ <TableShell
170
+ {...props}
171
+ {...defaults}
172
+ {...selection}
173
+ data={displayData}
174
+ sort={sort}
175
+ onSort={handleSort}
176
+ page={page}
177
+ onPageChange={setPage}
178
+ totalRows={ps > 0 ? allRows : (props.totalRows ?? allRows)}
179
+ />
180
+ )
181
+ }
182
+
183
+ function TableURLState<T extends Record<string, unknown>>(props: TableProps<T>) {
184
+ const defaults = applyDefaults(props)
185
+ const selection = useInternalSelection(props)
186
+ const router = useRouter()
187
+ const pathname = usePathname()
188
+ const searchParams = useSearchParams()
189
+
190
+ const urlColumn = searchParams.get('column') ?? ''
191
+ const urlOrder = (searchParams.get('order') ?? 'asc') as 'asc' | 'desc'
192
+ const urlPage = Math.max(1, parseInt(searchParams.get('page') ?? '1', 10) || 1)
193
+ const sort: SortState | undefined = urlColumn ? { column: urlColumn, order: urlOrder } : undefined
194
+
195
+ function handleSort(newSort: SortState) {
196
+ const p = new URLSearchParams(searchParams.toString())
197
+ p.set('column', newSort.column)
198
+ p.set('order', newSort.order)
199
+ p.set('page', '1')
200
+ router.replace(`${pathname}?${p.toString()}`)
201
+ }
202
+
203
+ function handlePageChange(newPage: number) {
204
+ const p = new URLSearchParams(searchParams.toString())
205
+ p.set('page', String(newPage))
206
+ router.replace(`${pathname}?${p.toString()}`)
207
+ }
208
+
209
+ return (
210
+ <TableShell
211
+ {...props}
212
+ {...defaults}
213
+ {...selection}
214
+ sort={sort}
215
+ onSort={handleSort}
216
+ page={urlPage}
217
+ onPageChange={handlePageChange}
218
+ totalRows={props.totalRows ?? props.data.length}
219
+ />
220
+ )
221
+ }
222
+
223
+ function TableControlled<T extends Record<string, unknown>>(props: TableProps<T>) {
224
+ const defaults = applyDefaults(props)
225
+ const selection = useInternalSelection(props)
226
+ return (
227
+ <TableShell
228
+ {...props}
229
+ {...defaults}
230
+ {...selection}
231
+ sort={props.sort}
232
+ onSort={props.onSort!}
233
+ page={props.page ?? 1}
234
+ onPageChange={props.onPageChange ?? (() => {})}
235
+ totalRows={props.totalRows ?? props.data.length}
236
+ />
237
+ )
238
+ }
239
+
240
+ export function Table<T extends Record<string, unknown> = Record<string, unknown>>(props: TableProps<T>) {
241
+ if (props.sort !== undefined || props.onSort !== undefined) {
242
+ return <TableControlled {...props} />
243
+ }
244
+
245
+ if (props.urlState) {
246
+ const defaults = applyDefaults(props)
247
+ const fallback = (
248
+ <TableShell
249
+ {...props}
250
+ {...defaults}
251
+ selectedIds={props.selectedIds ?? []}
252
+ onSelectionChange={props.onSelectionChange ?? (() => {})}
253
+ sort={undefined}
254
+ onSort={() => {}}
255
+ page={1}
256
+ onPageChange={() => {}}
257
+ totalRows={props.totalRows ?? props.data.length}
258
+ />
259
+ )
260
+ return (
261
+ <Suspense fallback={fallback}>
262
+ <TableURLState {...props} />
263
+ </Suspense>
264
+ )
265
+ }
266
+
267
+ return <TableLocalState {...props} />
268
+ }
269
+
270
+ export { MenuButton } from './menu'
271
+ export type { TableProps, Column, SortState, TableVariant, Density, TableColor } from './types'
@@ -0,0 +1,94 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type TableColor = 'green' | 'yellow' | 'red' | 'blue' | 'gray' | 'orange' | 'purple'
4
+
5
+ export type Column<T extends Record<string, unknown> = Record<string, unknown>> = {
6
+ key: keyof T & string
7
+ label?: string
8
+ sortable?: boolean
9
+ width?: string
10
+ align?: 'left' | 'center' | 'right'
11
+ highlight?: Record<string, TableColor>
12
+ render?: (value: unknown, row: T) => ReactNode
13
+ truncate?: boolean
14
+ }
15
+
16
+ export type SortState = {
17
+ column: string
18
+ order: 'asc' | 'desc'
19
+ }
20
+
21
+ export type Density = 'compact' | 'comfortable' | 'spacious'
22
+ export type TableVariant = 'original' | 'modern'
23
+
24
+ export type TableProps<T extends Record<string, unknown> = Record<string, unknown>> = {
25
+ data: T[]
26
+ columns: Column<T>[]
27
+
28
+ idKey?: keyof T & string
29
+
30
+ variant?: TableVariant
31
+ density?: Density
32
+ striped?: boolean
33
+ className?: string
34
+
35
+ loading?: boolean
36
+ loadingRows?: number
37
+ emptyState?: ReactNode
38
+
39
+ redirectPath?: string | { path: string; key?: string }
40
+ onRowClick?: (row: T, id: string) => void
41
+
42
+ renderExpandedRow?: (row: T) => ReactNode
43
+
44
+ selectable?: boolean
45
+ selectedIds?: string[]
46
+ onSelectionChange?: (ids: string[]) => void
47
+
48
+ // Controlled: provide sort + onSort (table does not manage sort state)
49
+ // URL: provide urlState={true} (syncs ?column=&order= params, Next.js only)
50
+ // Local: neither; manages sort state internally and sorts client-side
51
+ urlState?: boolean
52
+ sort?: SortState
53
+ onSort?: (sort: SortState) => void
54
+
55
+ // URL: totalRows from server, pageSize; table pushes ?page= to URL
56
+ // Local: paginates data internally; totalRows inferred from data.length
57
+ // Controlled: provide page + onPageChange + totalRows
58
+ pageSize?: number
59
+ totalRows?: number
60
+ page?: number
61
+ onPageChange?: (page: number) => void
62
+ hidePagination?: boolean
63
+
64
+ menuItems?: (row: T, id: string) => ReactNode
65
+ }
66
+
67
+ export type TableShellProps<T extends Record<string, unknown>> = {
68
+ data: T[]
69
+ columns: Column<T>[]
70
+ idKey?: keyof T & string
71
+ variant: TableVariant
72
+ density: Density
73
+ striped: boolean
74
+ loading: boolean
75
+ loadingRows: number
76
+ emptyState?: ReactNode
77
+ redirectPath?: string | { path: string; key?: string }
78
+ onRowClick?: (row: T, id: string) => void
79
+ renderExpandedRow?: (row: T) => ReactNode
80
+ selectable: boolean
81
+ selectedIds: string[]
82
+ onSelectionChange: (ids: string[]) => void
83
+ sort?: SortState
84
+ onSort: (sort: SortState) => void
85
+ page: number
86
+ onPageChange: (page: number) => void
87
+ totalRows: number
88
+ pageSize: number
89
+ hidePagination: boolean
90
+ menuItems?: (row: T, id: string) => ReactNode
91
+ className?: string
92
+ }
93
+
94
+ export type MenuAnchor = { top: number; right: number }
@@ -0,0 +1,12 @@
1
+ import type { Column } from './types'
2
+
3
+ export function resolveId<T extends Record<string, unknown>>(
4
+ row: T,
5
+ idKey: string | undefined,
6
+ columns: Column<T>[],
7
+ ): string {
8
+ if (idKey && row[idKey] !== undefined) return String(row[idKey])
9
+ if (row['id'] !== undefined) return String(row['id'])
10
+ const firstKey = columns[0]?.key ?? Object.keys(row)[0]
11
+ return String(row[firstKey] ?? '')
12
+ }
@@ -51,7 +51,6 @@ function ThemeIcon({ theme }: { theme: 'dark' | 'light' }) {
51
51
  viewBox='0 0 100 100'
52
52
  xmlns='http://www.w3.org/2000/svg'
53
53
  >
54
- {/* Sun */}
55
54
  <defs>
56
55
  <mask id='theme-toggle_clip-path'>
57
56
  <rect x='0' y='0' width='100' height='100' fill='white' />
@@ -65,7 +64,6 @@ function ThemeIcon({ theme }: { theme: 'dark' | 'light' }) {
65
64
  />
66
65
  </mask>
67
66
  </defs>
68
- {/* Moon */}
69
67
  <circle
70
68
  className={`origin-center transition-all duration-400 ${
71
69
  theme === 'light'
@@ -77,7 +75,6 @@ function ThemeIcon({ theme }: { theme: 'dark' | 'light' }) {
77
75
  cy='50'
78
76
  r='23'
79
77
  />
80
- {/* Sunrays */}
81
78
  <rect
82
79
  className={sunrayClass}
83
80
  x='86'
package/src/globals.css CHANGED
@@ -147,6 +147,17 @@ pre code.hljs {
147
147
  padding: 0;
148
148
  }
149
149
 
150
+ @keyframes shimmer {
151
+ 0% { background-position: -200% 0; }
152
+ 100% { background-position: 200% 0; }
153
+ }
154
+
155
+ @utility animate-shimmer {
156
+ background: linear-gradient(90deg, var(--color-login-600) 25%, var(--color-login-500) 50%, var(--color-login-600) 75%);
157
+ background-size: 200% 100%;
158
+ animation: shimmer 1.6s ease-in-out infinite;
159
+ }
160
+
150
161
  @keyframes jump {
151
162
  40% {
152
163
  transform: translateY(-0.3rem);
@@ -32,7 +32,6 @@ export default async function authCallback({
32
32
  }
33
33
 
34
34
  try {
35
- // Exchanges callback code for access token
36
35
  const tokenResponse = await fetch(tokenURL, {
37
36
  method: 'POST',
38
37
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -56,7 +55,6 @@ export default async function authCallback({
56
55
 
57
56
  const token = JSON.parse(tokenResponseBody)
58
57
 
59
- // Fetches user info using access token
60
58
  const userInfoResponse = await fetch(userInfoURL, {
61
59
  headers: { Authorization: `Bearer ${token.access_token}` }
62
60
  })
@@ -1,6 +1,5 @@
1
1
  export { LogoConsoleOutput } from './logoConsoleOutput/logoConsoleOutput'
2
2
 
3
- // Auth
4
3
  export { default as authLogin } from './auth/login'
5
4
  export { default as authCallback } from './auth/callback'
6
5
  export { default as authToken } from './auth/token'