@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.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/index.d.ts +1174 -0
  4. package/dist/index.js +5934 -0
  5. package/package.json +68 -0
  6. package/src/attribution/action.tsx +13 -0
  7. package/src/attribution/at.tsx +28 -0
  8. package/src/attribution/attribution.tsx +16 -0
  9. package/src/attribution/by.tsx +21 -0
  10. package/src/attribution/format-time.ts +48 -0
  11. package/src/attribution/index.ts +20 -0
  12. package/src/badge/badge.tsx +38 -0
  13. package/src/badge/index.ts +1 -0
  14. package/src/breadcrumbs/breadcrumbs-context.tsx +26 -0
  15. package/src/breadcrumbs/breadcrumbs.tsx +24 -0
  16. package/src/breadcrumbs/index.ts +12 -0
  17. package/src/breadcrumbs/item.tsx +85 -0
  18. package/src/button/animated-ellipsis.tsx +44 -0
  19. package/src/button/button.tsx +168 -0
  20. package/src/button/index.ts +1 -0
  21. package/src/callout/callout.tsx +202 -0
  22. package/src/callout/index.ts +2 -0
  23. package/src/callout/use-dismissed-callouts.ts +78 -0
  24. package/src/card/card.tsx +19 -0
  25. package/src/card/index.ts +2 -0
  26. package/src/checkbox/checkbox.tsx +76 -0
  27. package/src/checkbox/index.ts +1 -0
  28. package/src/combobox/button.tsx +99 -0
  29. package/src/combobox/combobox-context.tsx +42 -0
  30. package/src/combobox/combobox.tsx +60 -0
  31. package/src/combobox/empty.tsx +21 -0
  32. package/src/combobox/group-label.tsx +20 -0
  33. package/src/combobox/group.tsx +11 -0
  34. package/src/combobox/icon.tsx +11 -0
  35. package/src/combobox/index.tsx +64 -0
  36. package/src/combobox/input.tsx +77 -0
  37. package/src/combobox/item-indicator.tsx +16 -0
  38. package/src/combobox/item.tsx +58 -0
  39. package/src/combobox/list.tsx +19 -0
  40. package/src/combobox/popover.tsx +62 -0
  41. package/src/combobox/trigger.tsx +33 -0
  42. package/src/combobox/value.tsx +12 -0
  43. package/src/common/index.ts +1 -0
  44. package/src/common/svg.tsx +17 -0
  45. package/src/dialog/actions.tsx +17 -0
  46. package/src/dialog/confirm-button.tsx +25 -0
  47. package/src/dialog/dialog-context.tsx +21 -0
  48. package/src/dialog/dialog.tsx +37 -0
  49. package/src/dialog/dismiss-button.tsx +29 -0
  50. package/src/dialog/index.ts +37 -0
  51. package/src/dialog/message.tsx +20 -0
  52. package/src/dialog/provider.tsx +102 -0
  53. package/src/dialog/title.tsx +20 -0
  54. package/src/editable-cell/editable-cell.tsx +307 -0
  55. package/src/editable-cell/index.ts +7 -0
  56. package/src/editable-cell/use-editable-grid.tsx +296 -0
  57. package/src/field/field.tsx +22 -0
  58. package/src/field/index.ts +2 -0
  59. package/src/helpers/cn.ts +6 -0
  60. package/src/helpers/index.ts +1 -0
  61. package/src/icon-button/icon-button.tsx +126 -0
  62. package/src/icon-button/index.ts +2 -0
  63. package/src/index.ts +32 -0
  64. package/src/input/index.ts +1 -0
  65. package/src/input/input.tsx +276 -0
  66. package/src/link/index.ts +4 -0
  67. package/src/link/link-context.tsx +33 -0
  68. package/src/link/link.tsx +24 -0
  69. package/src/menu/divider.tsx +17 -0
  70. package/src/menu/index.ts +40 -0
  71. package/src/menu/item.tsx +149 -0
  72. package/src/menu/menu-context.tsx +24 -0
  73. package/src/menu/menu.tsx +14 -0
  74. package/src/menu/popover.tsx +69 -0
  75. package/src/menu/submenu-trigger.tsx +48 -0
  76. package/src/menu/submenu.tsx +20 -0
  77. package/src/menu/trigger.tsx +43 -0
  78. package/src/notification/index.ts +16 -0
  79. package/src/notification/list.tsx +123 -0
  80. package/src/notification/provider.tsx +75 -0
  81. package/src/page-header/index.ts +2 -0
  82. package/src/page-header/page-header.tsx +14 -0
  83. package/src/pagination/index.ts +1 -0
  84. package/src/pagination/pagination.tsx +136 -0
  85. package/src/panels/collapsed.tsx +32 -0
  86. package/src/panels/group.tsx +27 -0
  87. package/src/panels/index.ts +57 -0
  88. package/src/panels/layout-context.ts +5 -0
  89. package/src/panels/orientation-context.tsx +5 -0
  90. package/src/panels/panel.tsx +95 -0
  91. package/src/panels/separator.tsx +44 -0
  92. package/src/pin/index.ts +1 -0
  93. package/src/pin/pin.tsx +99 -0
  94. package/src/progress-bar/index.ts +1 -0
  95. package/src/progress-bar/progress-bar.tsx +48 -0
  96. package/src/quantity-input/index.ts +1 -0
  97. package/src/quantity-input/quantity-input.tsx +158 -0
  98. package/src/scroll-area/index.ts +1 -0
  99. package/src/scroll-area/scroll-area.tsx +82 -0
  100. package/src/slider/index.ts +1 -0
  101. package/src/slider/slider.tsx +75 -0
  102. package/src/star/index.ts +1 -0
  103. package/src/star/star.tsx +99 -0
  104. package/src/switch/index.ts +1 -0
  105. package/src/switch/switch.tsx +33 -0
  106. package/src/table/body.tsx +15 -0
  107. package/src/table/cell-context.tsx +48 -0
  108. package/src/table/cell.tsx +83 -0
  109. package/src/table/group-empty-state.tsx +28 -0
  110. package/src/table/group-header.tsx +62 -0
  111. package/src/table/header-cell.tsx +158 -0
  112. package/src/table/header-row.tsx +28 -0
  113. package/src/table/index.ts +56 -0
  114. package/src/table/is-group-empty-state.ts +17 -0
  115. package/src/table/is-group-header.ts +5 -0
  116. package/src/table/row-context.tsx +61 -0
  117. package/src/table/row-disabled-context.tsx +7 -0
  118. package/src/table/row.tsx +98 -0
  119. package/src/table/table-context.tsx +127 -0
  120. package/src/table/table-empty.tsx +57 -0
  121. package/src/table/table.tsx +547 -0
  122. package/src/table/tree-icon.tsx +147 -0
  123. package/src/table/use-checkbox.ts +200 -0
  124. package/src/table/use-column-layout.ts +117 -0
  125. package/src/table/use-groups.ts +134 -0
  126. package/src/table/use-keyboard-navigation.ts +274 -0
  127. package/src/table/use-sort-functions.ts +90 -0
  128. package/src/tabs/index.ts +20 -0
  129. package/src/tabs/list.tsx +29 -0
  130. package/src/tabs/tab.tsx +141 -0
  131. package/src/tabs/tabs-context.tsx +38 -0
  132. package/src/tabs/tabs.tsx +27 -0
  133. package/src/textarea/index.ts +1 -0
  134. package/src/textarea/textarea.tsx +88 -0
  135. package/src/toast/index.ts +19 -0
  136. package/src/toast/list.tsx +133 -0
  137. package/src/toast/provider.tsx +36 -0
  138. package/src/toggle/index.ts +17 -0
  139. package/src/toggle/indicator.tsx +27 -0
  140. package/src/toggle/item.tsx +54 -0
  141. package/src/toggle/toggle-context.tsx +68 -0
  142. package/src/toggle/toggle.tsx +124 -0
  143. package/src/toggle/use-indicator.tsx +69 -0
  144. package/src/tooltip/index.ts +1 -0
  145. package/src/tooltip/tooltip.tsx +122 -0
  146. 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,5 @@
1
+ import { TableNode } from '@table-library/react-table-library/types/table'
2
+
3
+ export const isGroupHeader = (item: TableNode) => {
4
+ return typeof item?.id === 'undefined'
5
+ }
@@ -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,7 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ const RowDisabledContext = createContext(false)
4
+
5
+ export const RowDisabledProvider = RowDisabledContext.Provider
6
+
7
+ export const useRowDisabled = () => useContext(RowDisabledContext)
@@ -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
+ }