@toolpath/ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.d.ts +1174 -0
- package/dist/index.js +5934 -0
- package/package.json +68 -0
- package/src/attribution/action.tsx +13 -0
- package/src/attribution/at.tsx +28 -0
- package/src/attribution/attribution.tsx +16 -0
- package/src/attribution/by.tsx +21 -0
- package/src/attribution/format-time.ts +48 -0
- package/src/attribution/index.ts +20 -0
- package/src/badge/badge.tsx +38 -0
- package/src/badge/index.ts +1 -0
- package/src/breadcrumbs/breadcrumbs-context.tsx +26 -0
- package/src/breadcrumbs/breadcrumbs.tsx +24 -0
- package/src/breadcrumbs/index.ts +12 -0
- package/src/breadcrumbs/item.tsx +85 -0
- package/src/button/animated-ellipsis.tsx +44 -0
- package/src/button/button.tsx +168 -0
- package/src/button/index.ts +1 -0
- package/src/callout/callout.tsx +202 -0
- package/src/callout/index.ts +2 -0
- package/src/callout/use-dismissed-callouts.ts +78 -0
- package/src/card/card.tsx +19 -0
- package/src/card/index.ts +2 -0
- package/src/checkbox/checkbox.tsx +76 -0
- package/src/checkbox/index.ts +1 -0
- package/src/combobox/button.tsx +99 -0
- package/src/combobox/combobox-context.tsx +42 -0
- package/src/combobox/combobox.tsx +60 -0
- package/src/combobox/empty.tsx +21 -0
- package/src/combobox/group-label.tsx +20 -0
- package/src/combobox/group.tsx +11 -0
- package/src/combobox/icon.tsx +11 -0
- package/src/combobox/index.tsx +64 -0
- package/src/combobox/input.tsx +77 -0
- package/src/combobox/item-indicator.tsx +16 -0
- package/src/combobox/item.tsx +58 -0
- package/src/combobox/list.tsx +19 -0
- package/src/combobox/popover.tsx +62 -0
- package/src/combobox/trigger.tsx +33 -0
- package/src/combobox/value.tsx +12 -0
- package/src/common/index.ts +1 -0
- package/src/common/svg.tsx +17 -0
- package/src/dialog/actions.tsx +17 -0
- package/src/dialog/confirm-button.tsx +25 -0
- package/src/dialog/dialog-context.tsx +21 -0
- package/src/dialog/dialog.tsx +37 -0
- package/src/dialog/dismiss-button.tsx +29 -0
- package/src/dialog/index.ts +37 -0
- package/src/dialog/message.tsx +20 -0
- package/src/dialog/provider.tsx +102 -0
- package/src/dialog/title.tsx +20 -0
- package/src/editable-cell/editable-cell.tsx +307 -0
- package/src/editable-cell/index.ts +7 -0
- package/src/editable-cell/use-editable-grid.tsx +296 -0
- package/src/field/field.tsx +22 -0
- package/src/field/index.ts +2 -0
- package/src/helpers/cn.ts +6 -0
- package/src/helpers/index.ts +1 -0
- package/src/icon-button/icon-button.tsx +126 -0
- package/src/icon-button/index.ts +2 -0
- package/src/index.ts +32 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.tsx +276 -0
- package/src/link/index.ts +4 -0
- package/src/link/link-context.tsx +33 -0
- package/src/link/link.tsx +24 -0
- package/src/menu/divider.tsx +17 -0
- package/src/menu/index.ts +40 -0
- package/src/menu/item.tsx +149 -0
- package/src/menu/menu-context.tsx +24 -0
- package/src/menu/menu.tsx +14 -0
- package/src/menu/popover.tsx +69 -0
- package/src/menu/submenu-trigger.tsx +48 -0
- package/src/menu/submenu.tsx +20 -0
- package/src/menu/trigger.tsx +43 -0
- package/src/notification/index.ts +16 -0
- package/src/notification/list.tsx +123 -0
- package/src/notification/provider.tsx +75 -0
- package/src/page-header/index.ts +2 -0
- package/src/page-header/page-header.tsx +14 -0
- package/src/pagination/index.ts +1 -0
- package/src/pagination/pagination.tsx +136 -0
- package/src/panels/collapsed.tsx +32 -0
- package/src/panels/group.tsx +27 -0
- package/src/panels/index.ts +57 -0
- package/src/panels/layout-context.ts +5 -0
- package/src/panels/orientation-context.tsx +5 -0
- package/src/panels/panel.tsx +95 -0
- package/src/panels/separator.tsx +44 -0
- package/src/pin/index.ts +1 -0
- package/src/pin/pin.tsx +99 -0
- package/src/progress-bar/index.ts +1 -0
- package/src/progress-bar/progress-bar.tsx +48 -0
- package/src/quantity-input/index.ts +1 -0
- package/src/quantity-input/quantity-input.tsx +158 -0
- package/src/scroll-area/index.ts +1 -0
- package/src/scroll-area/scroll-area.tsx +82 -0
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.tsx +75 -0
- package/src/star/index.ts +1 -0
- package/src/star/star.tsx +99 -0
- package/src/switch/index.ts +1 -0
- package/src/switch/switch.tsx +33 -0
- package/src/table/body.tsx +15 -0
- package/src/table/cell-context.tsx +48 -0
- package/src/table/cell.tsx +83 -0
- package/src/table/group-empty-state.tsx +28 -0
- package/src/table/group-header.tsx +62 -0
- package/src/table/header-cell.tsx +158 -0
- package/src/table/header-row.tsx +28 -0
- package/src/table/index.ts +56 -0
- package/src/table/is-group-empty-state.ts +17 -0
- package/src/table/is-group-header.ts +5 -0
- package/src/table/row-context.tsx +61 -0
- package/src/table/row-disabled-context.tsx +7 -0
- package/src/table/row.tsx +98 -0
- package/src/table/table-context.tsx +127 -0
- package/src/table/table-empty.tsx +57 -0
- package/src/table/table.tsx +547 -0
- package/src/table/tree-icon.tsx +147 -0
- package/src/table/use-checkbox.ts +200 -0
- package/src/table/use-column-layout.ts +117 -0
- package/src/table/use-groups.ts +134 -0
- package/src/table/use-keyboard-navigation.ts +274 -0
- package/src/table/use-sort-functions.ts +90 -0
- package/src/tabs/index.ts +20 -0
- package/src/tabs/list.tsx +29 -0
- package/src/tabs/tab.tsx +141 -0
- package/src/tabs/tabs-context.tsx +38 -0
- package/src/tabs/tabs.tsx +27 -0
- package/src/textarea/index.ts +1 -0
- package/src/textarea/textarea.tsx +88 -0
- package/src/toast/index.ts +19 -0
- package/src/toast/list.tsx +133 -0
- package/src/toast/provider.tsx +36 -0
- package/src/toggle/index.ts +17 -0
- package/src/toggle/indicator.tsx +27 -0
- package/src/toggle/item.tsx +54 -0
- package/src/toggle/toggle-context.tsx +68 -0
- package/src/toggle/toggle.tsx +124 -0
- package/src/toggle/use-indicator.tsx +69 -0
- package/src/tooltip/index.ts +1 -0
- package/src/tooltip/tooltip.tsx +122 -0
- package/theme.css +70 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
import { CaretRightIcon } from '@phosphor-icons/react'
|
|
4
|
+
import { useTable } from './table-context'
|
|
5
|
+
|
|
6
|
+
interface GroupHeaderProps {
|
|
7
|
+
groupIndex: number
|
|
8
|
+
rowIndex?: number
|
|
9
|
+
children: ReactNode
|
|
10
|
+
className?: string
|
|
11
|
+
inHeader?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const GroupHeader = ({
|
|
15
|
+
groupIndex,
|
|
16
|
+
children,
|
|
17
|
+
rowIndex,
|
|
18
|
+
inHeader: _inHeader = false,
|
|
19
|
+
className,
|
|
20
|
+
}: GroupHeaderProps) => {
|
|
21
|
+
const { columns, select, setCollapsedGroups, collapsedGroups, density } = useTable()
|
|
22
|
+
const isCollapsed = collapsedGroups.findIndex((item) => item === groupIndex) !== -1
|
|
23
|
+
|
|
24
|
+
const handleToggleGroup = () => {
|
|
25
|
+
setCollapsedGroups((current) => {
|
|
26
|
+
const foundIndex = current.findIndex((item) => item === groupIndex)
|
|
27
|
+
|
|
28
|
+
if (foundIndex !== -1) {
|
|
29
|
+
return current.filter((item) => item !== groupIndex)
|
|
30
|
+
} else {
|
|
31
|
+
return [...current, groupIndex]
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={handleToggleGroup}
|
|
40
|
+
tabIndex={-1}
|
|
41
|
+
data-row-index={rowIndex}
|
|
42
|
+
className={cn(
|
|
43
|
+
`w-full ${density === 'roomy' ? 'h-11' : 'h-8'} pt-1.5 bg-white dark:bg-zinc-900 flex flex-row items-center border-b border-gray-100 dark:border-zinc-800 gap-2 px-2 text-xs font-medium text-gray dark:text-zinc-100 truncate`,
|
|
44
|
+
// {
|
|
45
|
+
// 'border-t border-t-gray-50': !isPreviousCollapsed && !inHeader,
|
|
46
|
+
// },
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
style={{
|
|
50
|
+
gridColumn: `span ${columns + (select ? 1 : 0)}`,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<CaretRightIcon
|
|
54
|
+
className={cn(
|
|
55
|
+
'pointer-events-none fill-current text-gray dark:text-zinc-100 size-3 -rotate-90',
|
|
56
|
+
{ 'rotate-90': isCollapsed },
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
<span>{children}</span>
|
|
60
|
+
</button>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { HTMLAttributes, MouseEvent, ReactNode } from 'react'
|
|
3
|
+
import { HeaderCellSort } from '@table-library/react-table-library/sort'
|
|
4
|
+
import { HeaderCell as BaseHeaderCell, TableNode } from '@table-library/react-table-library/table'
|
|
5
|
+
import { HeaderCellSortProps } from '@table-library/react-table-library/types/sort'
|
|
6
|
+
import { cn } from '../helpers'
|
|
7
|
+
import { useTable } from './table-context'
|
|
8
|
+
|
|
9
|
+
const Divider = () => (
|
|
10
|
+
<div className="absolute top-0 right-0 w-1 h-full py-1 shrink-0">
|
|
11
|
+
<div className="w-1 h-full border-r border-gray-100 dark:border-zinc-800"></div>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const SortIcon = ({ flipped = false }: { flipped?: boolean }) => (
|
|
16
|
+
<div className="pr-2 text-center flex flex-row-reverse shrink-0">
|
|
17
|
+
<svg
|
|
18
|
+
className={cn('pointer-events-none fill-current text-gray dark:text-zinc-100 size-2', {
|
|
19
|
+
'rotate-180': flipped,
|
|
20
|
+
})}
|
|
21
|
+
viewBox="0 0 5 3"
|
|
22
|
+
>
|
|
23
|
+
<path d="M0 2.5L2.5 0L5 2.5H0Z" />
|
|
24
|
+
</svg>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export interface HeaderCellProps extends Omit<HeaderCellSortProps, 'sortKey'> {
|
|
29
|
+
sort?: boolean
|
|
30
|
+
sortFn?: (array: Array<TableNode>) => Array<TableNode>
|
|
31
|
+
width?: string | number
|
|
32
|
+
sortKey?: string
|
|
33
|
+
divider?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HeaderCellInteractiveProps extends HTMLAttributes<HTMLSpanElement> {
|
|
37
|
+
children: ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HeaderCellContentProps extends HTMLAttributes<HTMLSpanElement> {
|
|
41
|
+
accessory?: ReactNode
|
|
42
|
+
children: ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const HeaderCellContent = ({
|
|
46
|
+
accessory,
|
|
47
|
+
children,
|
|
48
|
+
className,
|
|
49
|
+
style,
|
|
50
|
+
...props
|
|
51
|
+
}: HeaderCellContentProps) => {
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
className={cn('gap-1', className)}
|
|
55
|
+
style={{
|
|
56
|
+
display: 'inline-flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'flex-start',
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
width: '100%',
|
|
61
|
+
textAlign: 'left',
|
|
62
|
+
...style,
|
|
63
|
+
}}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<span
|
|
67
|
+
style={{
|
|
68
|
+
display: 'block',
|
|
69
|
+
flex: '0 1 auto',
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
overflow: 'hidden',
|
|
72
|
+
textAlign: 'left',
|
|
73
|
+
textOverflow: 'ellipsis',
|
|
74
|
+
whiteSpace: 'nowrap',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</span>
|
|
79
|
+
{accessory}
|
|
80
|
+
</span>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const HeaderCellInteractive = ({
|
|
85
|
+
children,
|
|
86
|
+
className,
|
|
87
|
+
onClick,
|
|
88
|
+
onMouseDown,
|
|
89
|
+
style,
|
|
90
|
+
...props
|
|
91
|
+
}: HeaderCellInteractiveProps) => {
|
|
92
|
+
const handleClick = (event: MouseEvent<HTMLSpanElement>) => {
|
|
93
|
+
event.stopPropagation()
|
|
94
|
+
onClick?.(event)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleMouseDown = (event: MouseEvent<HTMLSpanElement>) => {
|
|
98
|
+
event.stopPropagation()
|
|
99
|
+
onMouseDown?.(event)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<span
|
|
104
|
+
className={cn('inline-flex shrink-0 items-center', className)}
|
|
105
|
+
onClick={handleClick}
|
|
106
|
+
onMouseDown={handleMouseDown}
|
|
107
|
+
style={{ ...style, pointerEvents: 'auto' }}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</span>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const HeaderCell = ({
|
|
116
|
+
children,
|
|
117
|
+
sortKey,
|
|
118
|
+
sortFn,
|
|
119
|
+
resize = { minWidth: 25, resizerWidth: 8 },
|
|
120
|
+
width: _width = '0px',
|
|
121
|
+
divider = true,
|
|
122
|
+
...props
|
|
123
|
+
}: HeaderCellProps) => {
|
|
124
|
+
const { columns, select, isEmpty, density } = useTable()
|
|
125
|
+
const isLastColumn = props.index === columns - (select ? 0 : 1)
|
|
126
|
+
|
|
127
|
+
const headerCellClass = 'border-b border-gray-100 dark:border-zinc-800 relative'
|
|
128
|
+
const titleClass = cn(
|
|
129
|
+
'flex flex-row items-center text-xs font-medium px-1.5 select-none',
|
|
130
|
+
density === 'roomy' ? 'h-11' : 'h-8',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (isEmpty || (!sortKey && !sortFn)) {
|
|
134
|
+
return (
|
|
135
|
+
<BaseHeaderCell className={headerCellClass} resize={resize} {...props}>
|
|
136
|
+
<div className={titleClass}>{children}</div>
|
|
137
|
+
{divider && !isLastColumn && <Divider />}
|
|
138
|
+
</BaseHeaderCell>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<HeaderCellSort
|
|
144
|
+
className={headerCellClass}
|
|
145
|
+
sortKey={props.index}
|
|
146
|
+
resize={resize}
|
|
147
|
+
sortIcon={{
|
|
148
|
+
iconDefault: null,
|
|
149
|
+
iconDown: <SortIcon flipped />,
|
|
150
|
+
iconUp: <SortIcon />,
|
|
151
|
+
}}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
<div className={cn(titleClass, 'min-w-0 w-full justify-start')}>{children}</div>
|
|
155
|
+
{divider && !isLastColumn && <Divider />}
|
|
156
|
+
</HeaderCellSort>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { HeaderRow as BaseHeaderRow } from '@table-library/react-table-library/table'
|
|
3
|
+
import { HeaderRowProps as BaseHeaderRowProps } from '@table-library/react-table-library/types/table'
|
|
4
|
+
import { cn } from '../helpers'
|
|
5
|
+
import { HeaderCell } from './header-cell'
|
|
6
|
+
import { useTable } from './table-context'
|
|
7
|
+
|
|
8
|
+
export interface HeaderRowProps extends BaseHeaderRowProps {
|
|
9
|
+
noOffset?: boolean
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const HeaderRow = ({ children, noOffset = false, className, ...props }: HeaderRowProps) => {
|
|
14
|
+
const { select, multiselect } = useTable()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<BaseHeaderRow className={cn('!bg-white dark:!bg-zinc-900', className)} {...props}>
|
|
18
|
+
{(select || multiselect) && !noOffset && (
|
|
19
|
+
<HeaderCell
|
|
20
|
+
resize={false}
|
|
21
|
+
divider={false}
|
|
22
|
+
className="stiff border-b border-gray-100 dark:border-zinc-800"
|
|
23
|
+
/>
|
|
24
|
+
)}
|
|
25
|
+
{children}
|
|
26
|
+
</BaseHeaderRow>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Cell } from './cell'
|
|
2
|
+
import { HeaderCell, HeaderCellContent, HeaderCellInteractive } from './header-cell'
|
|
3
|
+
import { HeaderRow } from './header-row'
|
|
4
|
+
import { Row } from './row'
|
|
5
|
+
import { useTable } from './table-context'
|
|
6
|
+
import { type TableDensity as TableDensityType } from './table-context'
|
|
7
|
+
import { useRow } from './row-context'
|
|
8
|
+
import { useCell } from './cell-context'
|
|
9
|
+
import { useCheckbox } from './use-checkbox'
|
|
10
|
+
import { TableEmpty } from './table-empty'
|
|
11
|
+
import {
|
|
12
|
+
TableRoot,
|
|
13
|
+
TableProps,
|
|
14
|
+
type TableSelectedRows as TableSelectedRowsType,
|
|
15
|
+
type TableSortState as TableSortStateType,
|
|
16
|
+
type TableSort as TableDefaultSortType,
|
|
17
|
+
type TableSortDirection as TableDefaultSortDirectionType,
|
|
18
|
+
} from './table'
|
|
19
|
+
import { ReactElement } from 'react'
|
|
20
|
+
|
|
21
|
+
export type TableSelectedRows = TableSelectedRowsType | undefined
|
|
22
|
+
export type TableSortState = TableSortStateType
|
|
23
|
+
export type TableDefaultSort = TableDefaultSortType
|
|
24
|
+
export type TableDefaultSortDirection = TableDefaultSortDirectionType
|
|
25
|
+
export type TableDensity = TableDensityType
|
|
26
|
+
|
|
27
|
+
type TableCompoundType = {
|
|
28
|
+
<T>(props: TableProps<T>): ReactElement
|
|
29
|
+
HeaderRow: typeof HeaderRow
|
|
30
|
+
HeaderCell: typeof HeaderCell
|
|
31
|
+
HeaderCellContent: typeof HeaderCellContent
|
|
32
|
+
HeaderCellInteractive: typeof HeaderCellInteractive
|
|
33
|
+
Row: typeof Row
|
|
34
|
+
Cell: typeof Cell
|
|
35
|
+
useTable: typeof useTable
|
|
36
|
+
useRow: typeof useRow
|
|
37
|
+
useCell: typeof useCell
|
|
38
|
+
useCheckbox: typeof useCheckbox
|
|
39
|
+
Empty: typeof TableEmpty
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TableCompound: TableCompoundType = Object.assign(TableRoot, {
|
|
43
|
+
HeaderRow,
|
|
44
|
+
HeaderCell,
|
|
45
|
+
HeaderCellContent,
|
|
46
|
+
HeaderCellInteractive,
|
|
47
|
+
Row,
|
|
48
|
+
Cell,
|
|
49
|
+
useTable,
|
|
50
|
+
useRow,
|
|
51
|
+
useCell,
|
|
52
|
+
useCheckbox,
|
|
53
|
+
Empty: TableEmpty,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export { TableCompound as Table }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TableNode } from '@table-library/react-table-library/types/table'
|
|
2
|
+
|
|
3
|
+
const GROUP_EMPTY_STATE_KEY = '__groupEmptyState'
|
|
4
|
+
const GROUP_EMPTY_STATE_SPACER_KEY = '__groupEmptyStateSpacer'
|
|
5
|
+
|
|
6
|
+
export const createGroupEmptyStateMarker = (spacer = false) => ({
|
|
7
|
+
[GROUP_EMPTY_STATE_KEY]: true,
|
|
8
|
+
[GROUP_EMPTY_STATE_SPACER_KEY]: spacer,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const isGroupEmptyState = (item: TableNode) => {
|
|
12
|
+
return item?.[GROUP_EMPTY_STATE_KEY] === true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const isGroupEmptyStateSpacer = (item: TableNode) => {
|
|
16
|
+
return item?.[GROUP_EMPTY_STATE_SPACER_KEY] === true
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
Dispatch,
|
|
3
|
+
FC,
|
|
4
|
+
ReactElement,
|
|
5
|
+
SetStateAction,
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import { useTable } from './table-context'
|
|
12
|
+
|
|
13
|
+
export interface RowContextInterface {
|
|
14
|
+
item: any
|
|
15
|
+
index: number
|
|
16
|
+
groupIndex?: number
|
|
17
|
+
isSelected: boolean
|
|
18
|
+
isOnlySelected: boolean
|
|
19
|
+
isDirectlySelected: boolean
|
|
20
|
+
isHovered: boolean
|
|
21
|
+
setIsHovered: Dispatch<SetStateAction<boolean>>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const RowContext = createContext<RowContextInterface>({} as RowContextInterface)
|
|
25
|
+
|
|
26
|
+
interface ProviderProps {
|
|
27
|
+
children: ReactElement
|
|
28
|
+
item: any
|
|
29
|
+
index: number
|
|
30
|
+
groupIndex?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RowProvider: FC<ProviderProps> = ({ item, index, groupIndex, children }) => {
|
|
34
|
+
const { selectedRows, select, cursor } = useTable()
|
|
35
|
+
// isOnlySelected means this row is selected AND it's the only one selected
|
|
36
|
+
// (id matches and ids array is empty)
|
|
37
|
+
const isOnlySelected = Boolean(selectedRows?.id === item.id) && !selectedRows?.ids?.length
|
|
38
|
+
const isSelected = isOnlySelected || Boolean(selectedRows?.ids?.includes(item.id))
|
|
39
|
+
const isDirectlySelected = isOnlySelected && index === cursor
|
|
40
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
41
|
+
|
|
42
|
+
const value = useMemo(
|
|
43
|
+
() => ({
|
|
44
|
+
item,
|
|
45
|
+
index,
|
|
46
|
+
groupIndex,
|
|
47
|
+
isOnlySelected,
|
|
48
|
+
isDirectlySelected,
|
|
49
|
+
isSelected,
|
|
50
|
+
isHovered: select ? isHovered : false,
|
|
51
|
+
setIsHovered,
|
|
52
|
+
}),
|
|
53
|
+
[item, index, groupIndex, isOnlySelected, isDirectlySelected, isSelected, select, isHovered],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return <RowContext.Provider value={value}>{children}</RowContext.Provider>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const useRow = () => useContext(RowContext)
|
|
60
|
+
const RowConsumer = RowContext.Consumer
|
|
61
|
+
export { useRow, RowProvider, RowContext, RowConsumer }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { memo, useEffect } from 'react'
|
|
2
|
+
import { Row as BaseRow, Cell } from '@table-library/react-table-library/table'
|
|
3
|
+
import { RowProps } from '@table-library/react-table-library/types/table'
|
|
4
|
+
import { cn } from '../helpers'
|
|
5
|
+
import { useRow } from './row-context'
|
|
6
|
+
import { RowDisabledProvider } from './row-disabled-context'
|
|
7
|
+
import { useTable } from './table-context'
|
|
8
|
+
|
|
9
|
+
export const Row = memo(
|
|
10
|
+
({
|
|
11
|
+
children,
|
|
12
|
+
onClick,
|
|
13
|
+
disabled = false,
|
|
14
|
+
...props
|
|
15
|
+
}: Omit<RowProps<any>, 'item'> & { disabled?: boolean }) => {
|
|
16
|
+
const { item, isSelected, setIsHovered, isHovered, index, isDirectlySelected } = useRow()
|
|
17
|
+
const { select, multiselect, setCursor, onBeforeSelectionChange, density } = useTable()
|
|
18
|
+
|
|
19
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
20
|
+
// Check if selection change is allowed before updating cursor
|
|
21
|
+
if (onBeforeSelectionChange) {
|
|
22
|
+
const allowed = onBeforeSelectionChange(item, index)
|
|
23
|
+
if (allowed === false) {
|
|
24
|
+
// Prevent the default selection behavior from the library
|
|
25
|
+
// Note: The library may pass a non-standard event, so check methods exist
|
|
26
|
+
if (typeof event?.stopPropagation === 'function') {
|
|
27
|
+
event.stopPropagation()
|
|
28
|
+
}
|
|
29
|
+
if (typeof event?.preventDefault === 'function') {
|
|
30
|
+
event.preventDefault()
|
|
31
|
+
}
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Set cursor to the clicked row's index
|
|
37
|
+
setCursor(index)
|
|
38
|
+
// Call any existing onClick handler
|
|
39
|
+
if (onClick) {
|
|
40
|
+
onClick(event)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleMouseEnter = () => {
|
|
45
|
+
setIsHovered(true)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleMouseLeave = () => {
|
|
49
|
+
// Immediately reset hover state
|
|
50
|
+
setIsHovered(false)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cleanup effect to ensure hover state is reset on unmount
|
|
54
|
+
// This handles cases where onMouseLeave doesn't fire when moving quickly
|
|
55
|
+
// or when rows are unmounted in virtualized tables
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
setIsHovered(false)
|
|
59
|
+
}
|
|
60
|
+
}, [setIsHovered])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<RowDisabledProvider value={disabled}>
|
|
64
|
+
<BaseRow
|
|
65
|
+
item={item}
|
|
66
|
+
onMouseEnter={handleMouseEnter}
|
|
67
|
+
onMouseLeave={handleMouseLeave}
|
|
68
|
+
onClick={handleClick}
|
|
69
|
+
data-row-index={index}
|
|
70
|
+
data-disabled={disabled || undefined}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{(select || multiselect) && (
|
|
74
|
+
<Cell className="border-b border-gray-50 dark:border-zinc-800 stiff">
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex flex-row-reverse items-center w-full',
|
|
78
|
+
density === 'roomy' ? 'h-11' : 'h-8',
|
|
79
|
+
{
|
|
80
|
+
'bg-gray-50 dark:bg-zinc-800': isSelected || isHovered,
|
|
81
|
+
'text-gray-300 dark:text-zinc-500': disabled,
|
|
82
|
+
},
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{isDirectlySelected && (
|
|
86
|
+
<div className="flex flex-row-reverse items-center h-7 text-xs font-medium">
|
|
87
|
+
<div className="w-1 h-full bg-info rounded-sm" />
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</Cell>
|
|
92
|
+
)}
|
|
93
|
+
{children}
|
|
94
|
+
</BaseRow>
|
|
95
|
+
</RowDisabledProvider>
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
Dispatch,
|
|
3
|
+
FC,
|
|
4
|
+
ReactElement,
|
|
5
|
+
SetStateAction,
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import { IdState } from '@table-library/react-table-library/types/common'
|
|
12
|
+
|
|
13
|
+
export type EditingCell = [number, number | string, number]
|
|
14
|
+
export type TableDensity = 'dense' | 'roomy'
|
|
15
|
+
|
|
16
|
+
export interface TableContextInterface {
|
|
17
|
+
columns: number
|
|
18
|
+
density: TableDensity
|
|
19
|
+
select: boolean
|
|
20
|
+
multiselect: boolean
|
|
21
|
+
keyboardNavigation: boolean
|
|
22
|
+
isEmpty: boolean
|
|
23
|
+
cursor: number | null
|
|
24
|
+
setCursor: Dispatch<SetStateAction<number | null>>
|
|
25
|
+
selectedRows: IdState
|
|
26
|
+
setSelectedRows: Dispatch<SetStateAction<IdState>>
|
|
27
|
+
editingCell: EditingCell | null
|
|
28
|
+
setEditingCell: Dispatch<SetStateAction<EditingCell | null>>
|
|
29
|
+
collapsedGroups: Array<number>
|
|
30
|
+
setCollapsedGroups: Dispatch<SetStateAction<Array<number>>>
|
|
31
|
+
/**
|
|
32
|
+
* Called before selection changes (on row click). Return false to cancel
|
|
33
|
+
* both cursor and selection updates. Useful for dirty state confirmation.
|
|
34
|
+
*/
|
|
35
|
+
onBeforeSelectionChange?: (item: any, index: number | null) => boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TableContext = createContext<TableContextInterface>({} as TableContextInterface)
|
|
39
|
+
|
|
40
|
+
interface ProviderProps {
|
|
41
|
+
children: ReactElement
|
|
42
|
+
density?: TableDensity
|
|
43
|
+
select?: boolean
|
|
44
|
+
multiselect?: boolean
|
|
45
|
+
keyboardNavigation?: boolean
|
|
46
|
+
selectedRows?: IdState
|
|
47
|
+
setSelectedRows?: Dispatch<SetStateAction<IdState>>
|
|
48
|
+
editingCell?: EditingCell
|
|
49
|
+
setEditingCell?: Dispatch<SetStateAction<EditingCell | null>>
|
|
50
|
+
defaultCollapsedGroups?: Array<number>
|
|
51
|
+
columns: number
|
|
52
|
+
isEmpty: boolean
|
|
53
|
+
onBeforeSelectionChange?: (item: any, index: number | null) => boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TableProvider: FC<ProviderProps> = ({
|
|
57
|
+
children,
|
|
58
|
+
density = 'dense',
|
|
59
|
+
select = false,
|
|
60
|
+
multiselect = false,
|
|
61
|
+
keyboardNavigation = true,
|
|
62
|
+
selectedRows: controlledSelectedRows,
|
|
63
|
+
setSelectedRows: setControlledSelectedRows,
|
|
64
|
+
editingCell: controlledEditingCell,
|
|
65
|
+
setEditingCell: setControlledEditingCell,
|
|
66
|
+
defaultCollapsedGroups = [],
|
|
67
|
+
columns,
|
|
68
|
+
isEmpty,
|
|
69
|
+
onBeforeSelectionChange,
|
|
70
|
+
}) => {
|
|
71
|
+
const [selectedRows, setSelectedRows] = useState({} as IdState)
|
|
72
|
+
const [editingCell, setEditingCell] = useState<EditingCell | null>(null)
|
|
73
|
+
const [cursor, setCursor] = useState<number | null>(null)
|
|
74
|
+
const [collapsedGroups, setCollapsedGroups] = useState<Array<number>>(
|
|
75
|
+
() => defaultCollapsedGroups,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const resolvedSelectedRows = setControlledSelectedRows ? controlledSelectedRows : selectedRows
|
|
79
|
+
const resolvedSetSelectedRows = setControlledSelectedRows
|
|
80
|
+
? setControlledSelectedRows
|
|
81
|
+
: setSelectedRows
|
|
82
|
+
const resolvedEditingCell = setControlledEditingCell ? controlledEditingCell : editingCell
|
|
83
|
+
const resolvedSetEditingCell = setControlledEditingCell
|
|
84
|
+
? setControlledEditingCell
|
|
85
|
+
: setEditingCell
|
|
86
|
+
|
|
87
|
+
const value = useMemo(
|
|
88
|
+
() => ({
|
|
89
|
+
select,
|
|
90
|
+
multiselect,
|
|
91
|
+
keyboardNavigation,
|
|
92
|
+
density,
|
|
93
|
+
columns,
|
|
94
|
+
isEmpty,
|
|
95
|
+
cursor,
|
|
96
|
+
setCursor,
|
|
97
|
+
selectedRows: resolvedSelectedRows ?? ({} as IdState),
|
|
98
|
+
setSelectedRows: resolvedSetSelectedRows,
|
|
99
|
+
editingCell: resolvedEditingCell ?? null,
|
|
100
|
+
setEditingCell: resolvedSetEditingCell,
|
|
101
|
+
collapsedGroups,
|
|
102
|
+
setCollapsedGroups,
|
|
103
|
+
onBeforeSelectionChange,
|
|
104
|
+
}),
|
|
105
|
+
[
|
|
106
|
+
select,
|
|
107
|
+
multiselect,
|
|
108
|
+
keyboardNavigation,
|
|
109
|
+
density,
|
|
110
|
+
columns,
|
|
111
|
+
isEmpty,
|
|
112
|
+
cursor,
|
|
113
|
+
resolvedSelectedRows,
|
|
114
|
+
resolvedSetSelectedRows,
|
|
115
|
+
resolvedEditingCell,
|
|
116
|
+
resolvedSetEditingCell,
|
|
117
|
+
collapsedGroups,
|
|
118
|
+
onBeforeSelectionChange,
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return <TableContext.Provider value={value}>{children}</TableContext.Provider>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const useTable = () => useContext(TableContext)
|
|
126
|
+
const TableConsumer = TableContext.Consumer
|
|
127
|
+
export { useTable, TableProvider, TableContext, TableConsumer }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { ReactElement, ReactNode, useMemo } from 'react'
|
|
2
|
+
import { Header } from '@table-library/react-table-library/table'
|
|
3
|
+
import { useTable } from './table-context'
|
|
4
|
+
|
|
5
|
+
export interface TableEmptyProps {
|
|
6
|
+
header?: ReactElement
|
|
7
|
+
emptyButtonLabel?: ReactNode
|
|
8
|
+
emptyButtonText?: string
|
|
9
|
+
onClickEmptyButton?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TableEmpty = ({
|
|
13
|
+
header,
|
|
14
|
+
emptyButtonLabel = 'No items to display',
|
|
15
|
+
emptyButtonText,
|
|
16
|
+
onClickEmptyButton,
|
|
17
|
+
}: TableEmptyProps) => {
|
|
18
|
+
const { columns, select } = useTable()
|
|
19
|
+
|
|
20
|
+
const showButton = useMemo(() => {
|
|
21
|
+
return Boolean(
|
|
22
|
+
typeof emptyButtonText === 'string' &&
|
|
23
|
+
emptyButtonText !== '' &&
|
|
24
|
+
typeof onClickEmptyButton === 'function',
|
|
25
|
+
)
|
|
26
|
+
}, [emptyButtonText, onClickEmptyButton])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{header && <Header>{header}</Header>}
|
|
31
|
+
<div
|
|
32
|
+
data-testid="table-empty-state"
|
|
33
|
+
className="flex flex-row items-center h-64 w-full min-h-32"
|
|
34
|
+
style={{
|
|
35
|
+
gridColumn: `span ${columns + (select ? 1 : 0)}`,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<div className="flex w-full flex-col items-center gap-2.5 text-center text-sm font-medium text-gray-400 dark:text-zinc-200 pb-10">
|
|
39
|
+
<label htmlFor={showButton ? 'empty-table-button' : undefined}>{emptyButtonLabel}</label>
|
|
40
|
+
{showButton && (
|
|
41
|
+
<button
|
|
42
|
+
aria-label={emptyButtonText}
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={onClickEmptyButton}
|
|
45
|
+
className="flex flex-col items-center group rounded outline-none font-medium cursor-pointer select-none transition duration-200 ease-in-out focus-visible:ring-2 focus-visible:ring-info/75 text-gray dark:text-zinc-200 focus-visible:bg-gray-100 dark:focus-visible:bg-zinc-800"
|
|
46
|
+
id="empty-table-button"
|
|
47
|
+
>
|
|
48
|
+
<div className="whitespace-nowrap rounded px-3 py-1 font-body text-xs font-medium tracking-normal bg-gray-100 group-hover:bg-gray-200/75 transition duration-200 ease-in-out group-active:scale-95 mx-auto dark:bg-zinc-800 dark:group-hover:bg-zinc-700">
|
|
49
|
+
{emptyButtonText}
|
|
50
|
+
</div>
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</>
|
|
56
|
+
)
|
|
57
|
+
}
|