uibee 3.1.2 → 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.
- package/dist/src/components/index.d.ts +59 -27
- package/dist/src/components/index.js +819 -336
- package/dist/style.css +283 -67
- package/package.json +1 -1
- package/src/components/index.ts +2 -17
- package/src/components/navbar/navbar.tsx +0 -5
- package/src/components/navbar/navbarDropdown.tsx +0 -2
- package/src/components/navbar/navbarItem.tsx +0 -2
- package/src/components/table/body.tsx +260 -153
- package/src/components/table/constants.ts +53 -0
- package/src/components/table/empty.tsx +31 -0
- package/src/components/table/format.ts +29 -22
- package/src/components/table/header.tsx +115 -70
- package/src/components/table/menu.tsx +43 -27
- package/src/components/table/pagination.tsx +162 -136
- package/src/components/table/skeleton.tsx +56 -0
- package/src/components/table/table.tsx +261 -34
- package/src/components/table/types.ts +94 -0
- package/src/components/table/utils.ts +12 -0
- package/src/components/toggle/theme.tsx +0 -3
- package/src/globals.css +11 -0
- package/src/utils/auth/callback.ts +0 -2
- package/src/utils/index.ts +0 -1
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
// Input components
|
|
3
2
|
export { default as Input } from './inputs/input'
|
|
4
3
|
export { default as SearchInput } from './inputs/searchInput'
|
|
5
4
|
export { default as Textarea } from './inputs/textarea'
|
|
@@ -11,26 +10,21 @@ export { default as Switch } from './inputs/switch'
|
|
|
11
10
|
export { default as Radio } from './inputs/radio'
|
|
12
11
|
export { default as Range } from './inputs/range'
|
|
13
12
|
|
|
14
|
-
// Logos
|
|
15
13
|
export { default as Logo } from './logo/logo'
|
|
16
14
|
export { default as LogoSmall } from './logo/logoSmall'
|
|
17
15
|
|
|
18
|
-
// Toggles
|
|
19
16
|
export { default as ThemeToggle } from './toggle/theme'
|
|
20
17
|
export { default as LanguageToggle } from './toggle/language'
|
|
21
18
|
|
|
22
|
-
// Navbar
|
|
23
19
|
export { default as Navbar } from './navbar/navbar'
|
|
24
20
|
export { default as NavItem } from './navbar/navbarItem'
|
|
25
21
|
export { default as NavDropdown } from './navbar/navbarDropdown'
|
|
26
22
|
|
|
27
|
-
// Footer
|
|
28
23
|
export { default as Footer } from './footer/footer'
|
|
29
24
|
export type { FooterColumn, FooterSocialLink, FooterProps, Lang, BilingualString } from './footer/footer'
|
|
30
25
|
export { loginAddress, loginEmail, loginCopyright, loginSponsor, loginSocialLinks } from './footer/loginContent'
|
|
31
26
|
export type { LoginSocialLinkData } from './footer/loginContent'
|
|
32
27
|
|
|
33
|
-
// Containers
|
|
34
28
|
export { default as PageContainer} from './container/page'
|
|
35
29
|
export { default as Highlight } from './container/highlight'
|
|
36
30
|
export { default as LeftBarPanel } from './container/leftBarPanel'
|
|
@@ -41,32 +35,23 @@ export { Tabs, TabPanel } from './container/tabs'
|
|
|
41
35
|
export { default as Accordion, AccordionGroup } from './container/accordion'
|
|
42
36
|
export { default as PulseDot } from './container/pulseDot'
|
|
43
37
|
|
|
44
|
-
// Login
|
|
45
38
|
export { default as VersionTag } from './version/version'
|
|
46
39
|
export { default as LoginPage } from './login/loginPage'
|
|
47
40
|
|
|
48
|
-
// Toasts
|
|
49
41
|
export { default as Toaster, toast } from './toast/toaster'
|
|
50
42
|
|
|
51
|
-
// Buttons
|
|
52
43
|
export { default as Button } from './buttons/button'
|
|
53
44
|
|
|
54
|
-
// Alert
|
|
55
45
|
export { default as Alert } from './alert/alert'
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
export { MenuButton } from './table/menu'
|
|
59
|
-
export { default as Table } from './table/table'
|
|
47
|
+
export { Table, MenuButton } from './table/table'
|
|
60
48
|
export { default as Pagination } from './table/pagination'
|
|
49
|
+
export type { TableProps, Column, SortState, TableVariant, Density, TableColor } from './table/types'
|
|
61
50
|
|
|
62
|
-
// Markdown
|
|
63
51
|
export { default as MarkdownRender } from './markdownrender/markdownRender'
|
|
64
52
|
|
|
65
|
-
// Confirm
|
|
66
53
|
export { default as ConfirmPopup } from './confirm/confirmPopup'
|
|
67
54
|
|
|
68
|
-
// Vulnerability
|
|
69
55
|
export { default as SeverityPill } from './vulnerability/severityPill'
|
|
70
56
|
|
|
71
|
-
// Layout
|
|
72
57
|
export { default as Toggle } from './inputs/toggle'
|
|
@@ -69,7 +69,6 @@ export default function Navbar({
|
|
|
69
69
|
<div className={`flex w-full max-w-6xl m-auto p-2 transition duration-500 800px:justify-between 800px:p-4 ${
|
|
70
70
|
isMobileMenuOpen ? 'h-screen bg-login-900/20 800px:h-20' : ''} ${innerClassName}
|
|
71
71
|
`}>
|
|
72
|
-
{/* Logo */}
|
|
73
72
|
<div className='block h-12 p-1 800px:p-0'>
|
|
74
73
|
<Link
|
|
75
74
|
href='/'
|
|
@@ -81,12 +80,10 @@ export default function Navbar({
|
|
|
81
80
|
|
|
82
81
|
{onlyLogo ? null : (
|
|
83
82
|
<>
|
|
84
|
-
{/* Desktop Navigation */}
|
|
85
83
|
<nav className='hidden 800px:flex 800px:justify-between 800px:items-center 800px:w-fill max-w-200'>
|
|
86
84
|
{children}
|
|
87
85
|
</nav>
|
|
88
86
|
|
|
89
|
-
{/* Controls */}
|
|
90
87
|
<nav className='flex w-[calc(100vw-8rem)] justify-end h-12 800px:w-fit'>
|
|
91
88
|
<div className='relative grid gap-2'>
|
|
92
89
|
{!disableThemeToggle &&
|
|
@@ -115,7 +112,6 @@ export default function Navbar({
|
|
|
115
112
|
</div>
|
|
116
113
|
</nav>
|
|
117
114
|
|
|
118
|
-
{/* Mobile Menu Button */}
|
|
119
115
|
<button
|
|
120
116
|
className='w-12 h-12 relative cursor-pointer bg-none border-none 800px:hidden'
|
|
121
117
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
@@ -124,7 +120,6 @@ export default function Navbar({
|
|
|
124
120
|
<div className={hamburgerStyle(isMobileMenuOpen, true)} />
|
|
125
121
|
</button>
|
|
126
122
|
|
|
127
|
-
{/* Mobile Navigation */}
|
|
128
123
|
<nav className={`fixed top-16 w-[calc(100%-2rem)] max-w-140 mx-auto left-0 right-0 800px:hidden
|
|
129
124
|
transition-all duration-500 ease-in-out overflow-hidden
|
|
130
125
|
${isMobileMenuOpen ? 'max-h-[calc(100vh-4rem)] opacity-100' : 'max-h-0 opacity-0'}`}
|
|
@@ -16,7 +16,6 @@ export default function NavDropdown({ children, title, className }: NavDropdownP
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<>
|
|
19
|
-
{/* Desktop Dropdown */}
|
|
20
19
|
<div className={'relative hidden 800px:block'}>
|
|
21
20
|
<div className='outline-none' tabIndex={0} ref={navItemRef}
|
|
22
21
|
onMouseEnter={() => setIsDesktopOpen(true)}
|
|
@@ -53,7 +52,6 @@ export default function NavDropdown({ children, title, className }: NavDropdownP
|
|
|
53
52
|
</div>
|
|
54
53
|
</div>
|
|
55
54
|
|
|
56
|
-
{/* Mobile Dropdown */}
|
|
57
55
|
<div className={'block 800px:hidden!'}>
|
|
58
56
|
<button
|
|
59
57
|
className={'bg-none border-none cursor-pointer w-full text-left'}
|
|
@@ -21,7 +21,6 @@ export default function NavItem({ href, children, external = false, target, rel,
|
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<>
|
|
24
|
-
{/* Desktop version */}
|
|
25
24
|
<Link {...linkProps} className='hidden 800px:flex'>
|
|
26
25
|
<li className={`${commonStyling} text-base leading-4 p-3 font-bold transition-colors link-corner-hover
|
|
27
26
|
group-[.dropdown]:p-2.5 group-[.dropdown]:pr-3 group-[.dropdown]:pl-1`
|
|
@@ -32,7 +31,6 @@ export default function NavItem({ href, children, external = false, target, rel,
|
|
|
32
31
|
</li>
|
|
33
32
|
</Link>
|
|
34
33
|
|
|
35
|
-
{/* Mobile version */}
|
|
36
34
|
<Link {...linkProps} className='800px:hidden'>
|
|
37
35
|
<li className={`${commonStyling} text-2xl leading-6 overflow-hidden w-auto pl-4 rounded-[0.3rem] transition-all
|
|
38
36
|
duration-600 opacity-100 h-16 py-5 group-[.dropdown]:p-0 group-[.dropdown]:text-lg group-[.dropdown]:h-auto
|
|
@@ -1,186 +1,293 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo, type RefObject } from 'react'
|
|
2
4
|
import { useRouter } from 'next/navigation'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import Menu from './menu'
|
|
7
|
-
import type { Column } from 'uibee/components'
|
|
5
|
+
import { EllipsisVertical, ChevronDown } from 'lucide-react'
|
|
6
|
+
import type { Column, Density, TableVariant, MenuAnchor, TableShellProps } from './types'
|
|
7
|
+
import { HIGHLIGHT, DENSITY_TD, VARIANT_TBODY, VARIANT_ROW_HOVER, VARIANT_ROW_STRIPED } from './constants'
|
|
8
8
|
import { formatValue } from './format'
|
|
9
|
+
import { resolveId } from './utils'
|
|
10
|
+
import Menu from './menu'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function useClickOutside(ref: RefObject<HTMLElement | null>, cb: () => void, enabled: boolean) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!enabled) return
|
|
15
|
+
function handler(e: MouseEvent) {
|
|
16
|
+
if (ref.current && !ref.current.contains(e.target as Node)) cb()
|
|
17
|
+
}
|
|
18
|
+
document.addEventListener('mousedown', handler)
|
|
19
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
20
|
+
}, [ref, cb, enabled])
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
function resolveRedirectUrl(
|
|
24
|
+
redirectPath: TableShellProps<Record<string, unknown>>['redirectPath'],
|
|
25
|
+
row: Record<string, unknown>,
|
|
26
|
+
id: string
|
|
27
|
+
): string | null {
|
|
28
|
+
if (!redirectPath) return null
|
|
29
|
+
const cfg = typeof redirectPath === 'string'
|
|
30
|
+
? { path: redirectPath, key: undefined }
|
|
31
|
+
: redirectPath
|
|
32
|
+
if (!cfg.path) return null
|
|
33
|
+
const rid = cfg.key ? String(row[cfg.key] ?? id) : id
|
|
34
|
+
return cfg.path.includes('?') ? `${cfg.path}${rid}` : `${cfg.path}/${rid}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Cell<T extends Record<string, unknown>>({
|
|
38
|
+
col,
|
|
39
|
+
row,
|
|
40
|
+
density,
|
|
41
|
+
}: {
|
|
42
|
+
col: Column<T>
|
|
43
|
+
row: T
|
|
44
|
+
density: Density
|
|
45
|
+
}) {
|
|
46
|
+
const value = row[col.key]
|
|
47
|
+
// Truncate: opt-in, only applied when col.truncate === true
|
|
48
|
+
const shouldTruncate = col.truncate === true
|
|
49
|
+
const align = col.align ?? 'left'
|
|
50
|
+
|
|
51
|
+
const wrapperAlign =
|
|
52
|
+
align === 'right' ? 'text-right' :
|
|
53
|
+
align === 'center' ? 'text-center' : ''
|
|
54
|
+
|
|
55
|
+
let content: React.ReactNode
|
|
56
|
+
|
|
57
|
+
if (col.render) {
|
|
58
|
+
// Custom renderer: caller is responsible for alignment
|
|
59
|
+
content = col.render(value, row)
|
|
60
|
+
} else if (col.highlight) {
|
|
61
|
+
const colorName = col.highlight[String(value)] ?? col.highlight['default']
|
|
62
|
+
const formatted = String(formatValue(col.key, value))
|
|
63
|
+
if (colorName) {
|
|
64
|
+
content = (
|
|
65
|
+
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${HIGHLIGHT[colorName]}`}>
|
|
66
|
+
{formatted}
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
} else {
|
|
70
|
+
content = (
|
|
71
|
+
<span className={shouldTruncate ? 'block truncate' : 'whitespace-nowrap'}>
|
|
72
|
+
{formatted}
|
|
73
|
+
</span>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
const formatted = formatValue(col.key, value)
|
|
78
|
+
content = (
|
|
79
|
+
<span className={shouldTruncate ? 'block truncate' : 'whitespace-nowrap'}>
|
|
80
|
+
{formatted === null || formatted === undefined ? '-' : String(formatted)}
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<td
|
|
87
|
+
style={col.width ? { width: col.width, flexShrink: 0 } : undefined}
|
|
88
|
+
className={`
|
|
89
|
+
${col.width ? '' : 'flex-1'} min-w-0 flex items-center text-sm text-login-75
|
|
90
|
+
${DENSITY_TD[density]}
|
|
91
|
+
`}
|
|
92
|
+
>
|
|
93
|
+
{/* w-full fills the column; text-align handles alignment */}
|
|
94
|
+
<div className={`min-w-0 w-full ${wrapperAlign}`}>
|
|
95
|
+
{content}
|
|
96
|
+
</div>
|
|
97
|
+
</td>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type BodyProps<T extends Record<string, unknown>> = Pick<
|
|
102
|
+
TableShellProps<T>,
|
|
103
|
+
'data' | 'columns' | 'idKey' | 'variant' | 'density' | 'striped' |
|
|
104
|
+
'redirectPath' | 'onRowClick' | 'renderExpandedRow' |
|
|
105
|
+
'selectable' | 'selectedIds' | 'onSelectionChange' |
|
|
106
|
+
'menuItems'
|
|
107
|
+
>
|
|
108
|
+
|
|
109
|
+
export default function Body<T extends Record<string, unknown>>({
|
|
110
|
+
data, columns, idKey, variant, density, striped,
|
|
111
|
+
redirectPath, onRowClick, renderExpandedRow,
|
|
112
|
+
selectable, selectedIds, onSelectionChange,
|
|
113
|
+
menuItems,
|
|
114
|
+
}: BodyProps<T>) {
|
|
22
115
|
const router = useRouter()
|
|
116
|
+
const [openMenuId, setOpenMenuId] = useState<string | null>(null)
|
|
117
|
+
const [anchor, setAnchor] = useState<MenuAnchor | null>(null)
|
|
118
|
+
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
119
|
+
|
|
23
120
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
24
121
|
const tbodyRef = useRef<HTMLTableSectionElement>(null)
|
|
25
122
|
const menuWasOpenOnMouseDown = useRef(false)
|
|
26
123
|
|
|
27
|
-
|
|
124
|
+
const closeMenu = useCallback(() => setOpenMenuId(null), [])
|
|
125
|
+
useClickOutside(menuRef as RefObject<HTMLElement>, closeMenu, openMenuId !== null)
|
|
28
126
|
|
|
29
127
|
useEffect(() => {
|
|
30
128
|
const el = tbodyRef.current
|
|
31
129
|
if (!el) return
|
|
32
|
-
|
|
33
|
-
el.
|
|
34
|
-
|
|
35
|
-
|
|
130
|
+
el.addEventListener('scroll', closeMenu)
|
|
131
|
+
return () => el.removeEventListener('scroll', closeMenu)
|
|
132
|
+
}, [closeMenu])
|
|
133
|
+
|
|
134
|
+
function openMenu(id: string, coords: MenuAnchor) {
|
|
135
|
+
setAnchor(coords)
|
|
136
|
+
setOpenMenuId(id)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds])
|
|
140
|
+
|
|
141
|
+
function toggleSelect(id: string) {
|
|
142
|
+
const next = selectedSet.has(id)
|
|
143
|
+
? selectedIds.filter(s => s !== id)
|
|
144
|
+
: [...selectedIds, id]
|
|
145
|
+
onSelectionChange(next)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasMenu = Boolean(menuItems)
|
|
149
|
+
const hasExpand = Boolean(renderExpandedRow)
|
|
36
150
|
|
|
37
151
|
return (
|
|
38
152
|
<tbody
|
|
39
153
|
ref={tbodyRef}
|
|
40
|
-
className={`
|
|
41
|
-
block overflow-y-auto flex-1 min-h-0 divide-y
|
|
42
|
-
${variant === 'default' ? 'bg-login-800 divide-login-600/25' : 'bg-transparent divide-login-600/20'}
|
|
43
|
-
`}
|
|
154
|
+
className={`block overflow-y-auto flex-1 min-h-0 divide-y ${VARIANT_TBODY[variant]}`}
|
|
44
155
|
>
|
|
45
|
-
{
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const menuButtonColors = variant === 'minimal'
|
|
62
|
-
? {
|
|
63
|
-
active: 'bg-login-600 text-login-100',
|
|
64
|
-
inactive: 'hover:bg-login-600 text-login-200 hover:text-login-100'
|
|
65
|
-
}
|
|
66
|
-
: {
|
|
67
|
-
active: 'bg-login-400 text-login-100',
|
|
68
|
-
inactive: 'hover:bg-login-400 text-login-200 hover:text-login-100'
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const buttonClass = openMenuId === id ? menuButtonColors.active : menuButtonColors.inactive
|
|
156
|
+
{data.map((row, rowIdx) => {
|
|
157
|
+
const id = resolveId(row as Record<string, unknown>, idKey as string | undefined, columns as Column<Record<string, unknown>>[])
|
|
158
|
+
const url = resolveRedirectUrl(redirectPath, row as Record<string, unknown>, id)
|
|
159
|
+
const isClickable = Boolean(url || onRowClick || hasExpand)
|
|
160
|
+
const isMenuOpen = openMenuId === id
|
|
161
|
+
const isExpanded = expandedId === id
|
|
162
|
+
const isSelected = selectable && selectedSet.has(id)
|
|
163
|
+
const expandedContent = renderExpandedRow?.(row)
|
|
164
|
+
|
|
165
|
+
const rowClass = [
|
|
166
|
+
'flex w-full transition-colors duration-100',
|
|
167
|
+
isClickable ? 'cursor-pointer' : '',
|
|
168
|
+
VARIANT_ROW_HOVER[variant],
|
|
169
|
+
striped ? VARIANT_ROW_STRIPED[variant] : '',
|
|
170
|
+
isSelected ? 'bg-login/5' : '',
|
|
171
|
+
].filter(Boolean).join(' ')
|
|
72
172
|
|
|
73
173
|
return (
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
onClick={() => {
|
|
86
|
-
if (menuWasOpenOnMouseDown.current) {
|
|
87
|
-
menuWasOpenOnMouseDown.current = false
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (redirectConfig.path) {
|
|
91
|
-
if (redirectConfig.path.includes('?')) {
|
|
92
|
-
router.push(`${redirectConfig.path}${redirectId}`)
|
|
93
|
-
} else {
|
|
94
|
-
router.push(`${redirectConfig.path}/${redirectId}`)
|
|
174
|
+
<React.Fragment key={id + rowIdx}>
|
|
175
|
+
<tr
|
|
176
|
+
className={rowClass}
|
|
177
|
+
onMouseEnter={() => { if (url) router.prefetch(url) }}
|
|
178
|
+
onMouseDown={() => {
|
|
179
|
+
menuWasOpenOnMouseDown.current = openMenuId !== null
|
|
180
|
+
}}
|
|
181
|
+
onClick={() => {
|
|
182
|
+
if (menuWasOpenOnMouseDown.current) {
|
|
183
|
+
menuWasOpenOnMouseDown.current = false
|
|
184
|
+
return
|
|
95
185
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
e.preventDefault()
|
|
100
|
-
setAnchor({ top: e.clientY, right: window.innerWidth - e.clientX })
|
|
101
|
-
setOpenMenuId(id)
|
|
102
|
-
}}
|
|
103
|
-
>
|
|
104
|
-
{columns.map((col) => {
|
|
105
|
-
const val = itemRecord[col.key] as string | number | undefined
|
|
106
|
-
const value = val
|
|
107
|
-
|
|
108
|
-
let badgeClass = ''
|
|
109
|
-
if (col.highlight) {
|
|
110
|
-
const highlightKey = String(value)
|
|
111
|
-
const colorName = col.highlight[highlightKey] || col.highlight.default
|
|
112
|
-
if (colorName) {
|
|
113
|
-
switch (colorName) {
|
|
114
|
-
case 'green':
|
|
115
|
-
badgeClass = 'bg-green-500/20 text-green-400'; break
|
|
116
|
-
case 'yellow':
|
|
117
|
-
badgeClass = 'bg-yellow-500/20 text-yellow-400'; break
|
|
118
|
-
case 'red':
|
|
119
|
-
badgeClass = 'bg-red-500/20 text-red-400'; break
|
|
120
|
-
case 'blue':
|
|
121
|
-
badgeClass = 'bg-blue-500/20 text-blue-400'; break
|
|
122
|
-
case 'gray':
|
|
123
|
-
badgeClass = 'bg-gray-500/20 text-gray-400'; break
|
|
124
|
-
}
|
|
125
|
-
badgeClass += ' px-2 py-1 rounded'
|
|
186
|
+
if (hasExpand) {
|
|
187
|
+
setExpandedId(isExpanded ? null : id)
|
|
188
|
+
return
|
|
126
189
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{formatValue(col.key, value as string | number)}
|
|
140
|
-
</span>
|
|
141
|
-
</div>
|
|
142
|
-
</td>
|
|
143
|
-
)
|
|
144
|
-
})}
|
|
145
|
-
{menuItems && (
|
|
146
|
-
<td
|
|
147
|
-
className='shrink-0 w-16 flex flex-row items-center justify-end p-2 px-4
|
|
148
|
-
whitespace-nowrap text-right text-sm font-medium'
|
|
149
|
-
>
|
|
150
|
-
<div className='relative'>
|
|
190
|
+
if (onRowClick) { onRowClick(row, id); return }
|
|
191
|
+
if (url) router.push(url)
|
|
192
|
+
}}
|
|
193
|
+
onContextMenu={(e) => {
|
|
194
|
+
if (!hasMenu) return
|
|
195
|
+
e.preventDefault()
|
|
196
|
+
openMenu(id, { top: e.clientY, right: window.innerWidth - e.clientX })
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{selectable && (
|
|
200
|
+
<td className='flex items-center justify-center'
|
|
201
|
+
style={{ width: '3rem', minWidth: '3rem', flexShrink: 0 }}>
|
|
151
202
|
<button
|
|
152
203
|
type='button'
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
204
|
+
aria-label={isSelected ? 'Deselect row' : 'Select row'}
|
|
205
|
+
aria-checked={isSelected}
|
|
206
|
+
role='checkbox'
|
|
207
|
+
onClick={(e) => { e.stopPropagation(); toggleSelect(id) }}
|
|
208
|
+
className={`
|
|
209
|
+
h-4 w-4 rounded border flex items-center justify-center transition-colors cursor-pointer
|
|
210
|
+
${isSelected
|
|
211
|
+
? 'bg-login border-login text-white'
|
|
212
|
+
: 'border-login-400 bg-transparent hover:border-login-100'
|
|
213
|
+
}
|
|
214
|
+
`}
|
|
162
215
|
>
|
|
163
|
-
|
|
164
|
-
className='
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
{isSelected && (
|
|
217
|
+
<svg className='h-3 w-3' viewBox='0 0 12 12' fill='none'>
|
|
218
|
+
<path d='M2 6l3 3 5-5' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
|
219
|
+
</svg>
|
|
220
|
+
)}
|
|
168
221
|
</button>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
222
|
+
</td>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{columns.map((col) => (
|
|
226
|
+
<Cell key={col.key} col={col} row={row} density={density} />
|
|
227
|
+
))}
|
|
228
|
+
|
|
229
|
+
{hasExpand && (
|
|
230
|
+
<td className='flex items-center justify-center'
|
|
231
|
+
style={{ width: '2.5rem', minWidth: '2.5rem', flexShrink: 0 }}>
|
|
232
|
+
<ChevronDown className={`
|
|
233
|
+
h-4 w-4 transition-transform duration-200
|
|
234
|
+
${isExpanded ? 'rotate-180 text-login' : 'text-login-400'}
|
|
235
|
+
`} />
|
|
236
|
+
</td>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{hasMenu && (
|
|
240
|
+
<td className='flex items-center justify-end pr-3'
|
|
241
|
+
style={{ width: '3.5rem', minWidth: '3.5rem', flexShrink: 0 }}>
|
|
242
|
+
<div className='relative'>
|
|
243
|
+
<button
|
|
244
|
+
type='button'
|
|
245
|
+
aria-label='Row actions'
|
|
246
|
+
aria-expanded={isMenuOpen}
|
|
247
|
+
aria-haspopup='menu'
|
|
248
|
+
onMouseDown={(e) => e.nativeEvent.stopImmediatePropagation()}
|
|
249
|
+
onClick={(e) => {
|
|
250
|
+
e.stopPropagation()
|
|
251
|
+
if (isMenuOpen) {
|
|
252
|
+
setOpenMenuId(null)
|
|
253
|
+
} else {
|
|
254
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
255
|
+
openMenu(id, { top: rect.bottom + 4, right: window.innerWidth - rect.right })
|
|
256
|
+
}
|
|
257
|
+
}}
|
|
258
|
+
className={`
|
|
259
|
+
p-1.5 rounded flex items-center justify-center transition-colors
|
|
260
|
+
${isMenuOpen
|
|
261
|
+
? 'bg-login-500 text-login-75'
|
|
262
|
+
: 'text-login-300 hover:bg-login-500/60 hover:text-login-75'
|
|
263
|
+
}
|
|
264
|
+
`}
|
|
174
265
|
>
|
|
175
|
-
|
|
176
|
-
</
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
266
|
+
<EllipsisVertical className='h-4 w-4' />
|
|
267
|
+
</button>
|
|
268
|
+
{isMenuOpen && anchor && (
|
|
269
|
+
<Menu ref={menuRef} anchor={anchor} onClose={closeMenu}>
|
|
270
|
+
{menuItems!(row, id)}
|
|
271
|
+
</Menu>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</td>
|
|
275
|
+
)}
|
|
276
|
+
</tr>
|
|
277
|
+
|
|
278
|
+
{hasExpand && isExpanded && (
|
|
279
|
+
<tr className='flex w-full bg-login-700/25 border-b border-login-600/20'>
|
|
280
|
+
<td
|
|
281
|
+
className='w-full px-6 py-4'
|
|
282
|
+
onClick={(e) => e.stopPropagation()}
|
|
283
|
+
>
|
|
284
|
+
{expandedContent}
|
|
285
|
+
</td>
|
|
286
|
+
</tr>
|
|
180
287
|
)}
|
|
181
|
-
</
|
|
288
|
+
</React.Fragment>
|
|
182
289
|
)
|
|
183
290
|
})}
|
|
184
291
|
</tbody>
|
|
185
292
|
)
|
|
186
|
-
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TableColor, Density, TableVariant } from './types'
|
|
2
|
+
|
|
3
|
+
export const HIGHLIGHT: Record<TableColor, string> = {
|
|
4
|
+
green: 'bg-green-500/15 text-green-400 ring-1 ring-green-500/25',
|
|
5
|
+
yellow: 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/25',
|
|
6
|
+
red: 'bg-red-500/15 text-red-400 ring-1 ring-red-500/25',
|
|
7
|
+
blue: 'bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/25',
|
|
8
|
+
gray: 'bg-gray-500/15 text-gray-400 ring-1 ring-gray-500/25',
|
|
9
|
+
orange: 'bg-orange-500/15 text-orange-400 ring-1 ring-orange-500/25',
|
|
10
|
+
purple: 'bg-purple-500/15 text-purple-400 ring-1 ring-purple-500/25',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DENSITY_TH: Record<Density, string> = {
|
|
14
|
+
compact: 'px-4 py-2',
|
|
15
|
+
comfortable: 'px-6 py-3',
|
|
16
|
+
spacious: 'px-8 py-4',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DENSITY_TD: Record<Density, string> = {
|
|
20
|
+
compact: 'px-4 py-1.5',
|
|
21
|
+
comfortable: 'px-6 py-3.5',
|
|
22
|
+
spacious: 'px-8 py-5',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const VARIANT_CONTAINER: Record<TableVariant, string> = {
|
|
26
|
+
original: 'bg-login-500/50 rounded-lg border border-login-600 shadow',
|
|
27
|
+
modern: 'bg-transparent',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const VARIANT_THEAD: Record<TableVariant, string> = {
|
|
31
|
+
original: 'bg-login-700',
|
|
32
|
+
modern: 'bg-transparent border-b border-login-500/40',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const VARIANT_THEAD_TH: Record<TableVariant, string> = {
|
|
36
|
+
original: 'text-login-200',
|
|
37
|
+
modern: 'text-login-300',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const VARIANT_TBODY: Record<TableVariant, string> = {
|
|
41
|
+
original: 'bg-login-500/50 divide-login-600',
|
|
42
|
+
modern: 'divide-login-600/15',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const VARIANT_ROW_HOVER: Record<TableVariant, string> = {
|
|
46
|
+
original: 'hover:bg-login-600/30',
|
|
47
|
+
modern: 'hover:bg-login-700/50',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const VARIANT_ROW_STRIPED: Record<TableVariant, string> = {
|
|
51
|
+
original: 'even:bg-login-600/40',
|
|
52
|
+
modern: 'even:bg-login-800/40',
|
|
53
|
+
}
|