@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,274 @@
|
|
|
1
|
+
import { isNull } from 'lodash-es'
|
|
2
|
+
import { RefObject, useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import { getRowHeight } from './table'
|
|
4
|
+
import { useTable } from './table-context'
|
|
5
|
+
|
|
6
|
+
export const useKeyboardNavigation = (
|
|
7
|
+
indexes: Array<[number, number | string]>,
|
|
8
|
+
containerRef?: RefObject<HTMLElement | null>,
|
|
9
|
+
data?: Array<any>,
|
|
10
|
+
) => {
|
|
11
|
+
const {
|
|
12
|
+
cursor,
|
|
13
|
+
setCursor,
|
|
14
|
+
setSelectedRows,
|
|
15
|
+
selectedRows,
|
|
16
|
+
editingCell,
|
|
17
|
+
setEditingCell,
|
|
18
|
+
multiselect,
|
|
19
|
+
keyboardNavigation,
|
|
20
|
+
density,
|
|
21
|
+
onBeforeSelectionChange,
|
|
22
|
+
} = useTable()
|
|
23
|
+
|
|
24
|
+
const rowHeight = getRowHeight(density)
|
|
25
|
+
|
|
26
|
+
// Keep data ref for onBeforeSelectionChange lookups
|
|
27
|
+
const dataRef = useRef(data)
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
dataRef.current = data
|
|
30
|
+
}, [data])
|
|
31
|
+
|
|
32
|
+
// Use refs to access current values in event handlers without re-registering
|
|
33
|
+
const cursorRef = useRef(cursor)
|
|
34
|
+
const indexesRef = useRef(indexes)
|
|
35
|
+
const selectedRowsRef = useRef(selectedRows)
|
|
36
|
+
const multiselectRef = useRef(multiselect)
|
|
37
|
+
const editingCellRef = useRef(editingCell)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
cursorRef.current = cursor
|
|
40
|
+
}, [cursor])
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
indexesRef.current = indexes
|
|
43
|
+
}, [indexes])
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
selectedRowsRef.current = selectedRows
|
|
46
|
+
}, [selectedRows])
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
multiselectRef.current = multiselect
|
|
49
|
+
}, [multiselect])
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
editingCellRef.current = editingCell
|
|
52
|
+
}, [editingCell])
|
|
53
|
+
|
|
54
|
+
const isFocusWithinContainer = useCallback(() => {
|
|
55
|
+
if (!containerRef?.current) {
|
|
56
|
+
return true // If no container ref provided, allow navigation everywhere (backwards compatible)
|
|
57
|
+
}
|
|
58
|
+
return containerRef.current.contains(document.activeElement)
|
|
59
|
+
}, [containerRef])
|
|
60
|
+
|
|
61
|
+
const scrollRowIntoView = useCallback(
|
|
62
|
+
(rowIndex: number) => {
|
|
63
|
+
if (!containerRef?.current) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Access the virtualized table's scroll container
|
|
68
|
+
const scrollContainer = containerRef.current.querySelector(
|
|
69
|
+
'[data-table-library_table] > div > div',
|
|
70
|
+
) as HTMLElement | null
|
|
71
|
+
|
|
72
|
+
if (!scrollContainer) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const scrollTop = scrollContainer.scrollTop
|
|
77
|
+
const containerHeight = scrollContainer.clientHeight
|
|
78
|
+
const rowTop = rowIndex * rowHeight
|
|
79
|
+
const rowBottom = rowTop + rowHeight
|
|
80
|
+
|
|
81
|
+
// Check if row is outside the visible area
|
|
82
|
+
if (rowTop < scrollTop || rowBottom > scrollTop + containerHeight) {
|
|
83
|
+
// Scroll to center the row in the container
|
|
84
|
+
const rowCenter = rowTop + rowHeight / 2
|
|
85
|
+
const targetScrollTop = rowCenter - containerHeight / 2
|
|
86
|
+
scrollContainer.scrollTop = Math.max(0, targetScrollTop)
|
|
87
|
+
}
|
|
88
|
+
// Otherwise row is already visible, don't scroll
|
|
89
|
+
},
|
|
90
|
+
[containerRef, rowHeight],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const getNewSelectionState = useCallback((newId: number | string, addToSelection: boolean) => {
|
|
94
|
+
if (!addToSelection || !multiselectRef.current) {
|
|
95
|
+
return { ids: [], id: newId }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// When ctrl/cmd is held and multiselect is enabled, preserve existing selections
|
|
99
|
+
const currentSelected = selectedRowsRef.current
|
|
100
|
+
const existingIds: Array<number | string> = []
|
|
101
|
+
|
|
102
|
+
// Collect all currently selected IDs
|
|
103
|
+
if (currentSelected?.id != null) {
|
|
104
|
+
existingIds.push(currentSelected.id)
|
|
105
|
+
}
|
|
106
|
+
if (currentSelected?.ids?.length) {
|
|
107
|
+
existingIds.push(...currentSelected.ids)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add new ID if not already selected, or toggle it off if it is
|
|
111
|
+
const newIdIndex = existingIds.indexOf(newId)
|
|
112
|
+
if (newIdIndex === -1) {
|
|
113
|
+
existingIds.push(newId)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Deduplicate
|
|
117
|
+
const uniqueIds = Array.from(new Set(existingIds))
|
|
118
|
+
// When using ids for multi-select, id should be null (they're mutually exclusive)
|
|
119
|
+
return { ids: uniqueIds, id: null }
|
|
120
|
+
}, [])
|
|
121
|
+
|
|
122
|
+
// Keep onBeforeSelectionChange ref to avoid re-registering callbacks
|
|
123
|
+
const onBeforeSelectionChangeRef = useRef(onBeforeSelectionChange)
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
onBeforeSelectionChangeRef.current = onBeforeSelectionChange
|
|
126
|
+
}, [onBeforeSelectionChange])
|
|
127
|
+
|
|
128
|
+
// Helper to check if selection change is allowed (e.g., dirty state confirmation)
|
|
129
|
+
// Pass null for newId/newRowIndex when clearing selection (e.g., Escape key)
|
|
130
|
+
const isSelectionChangeAllowed = useCallback(
|
|
131
|
+
(newId: number | string | null, newRowIndex: number | null): boolean => {
|
|
132
|
+
if (!onBeforeSelectionChangeRef.current) {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
const item = newId != null ? dataRef.current?.find((d) => d.id === newId) : null
|
|
136
|
+
return onBeforeSelectionChangeRef.current(item, newRowIndex) !== false
|
|
137
|
+
},
|
|
138
|
+
[],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const increaseCursor = useCallback(
|
|
142
|
+
(addToSelection = false) => {
|
|
143
|
+
const currentIndexes = indexesRef.current
|
|
144
|
+
const currentCursor = cursorRef.current
|
|
145
|
+
if (currentIndexes.length === 0) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
const currentPosition = currentIndexes.findIndex((item) => item[0] === currentCursor)
|
|
149
|
+
const newPosition = currentPosition + 1 >= currentIndexes.length ? 0 : currentPosition + 1
|
|
150
|
+
const newRowIndex = currentIndexes[newPosition][0]
|
|
151
|
+
const newId = currentIndexes[newPosition][1]
|
|
152
|
+
|
|
153
|
+
if (!isSelectionChangeAllowed(newId, newRowIndex)) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setSelectedRows(getNewSelectionState(newId, addToSelection))
|
|
158
|
+
scrollRowIntoView(newRowIndex)
|
|
159
|
+
},
|
|
160
|
+
[setSelectedRows, scrollRowIntoView, getNewSelectionState, isSelectionChangeAllowed],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const decreaseCursor = useCallback(
|
|
164
|
+
(addToSelection = false) => {
|
|
165
|
+
const currentIndexes = indexesRef.current
|
|
166
|
+
const currentCursor = cursorRef.current
|
|
167
|
+
if (currentIndexes.length === 0) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
const currentPosition = currentIndexes.findIndex((item) => item[0] === currentCursor)
|
|
171
|
+
const newPosition = currentPosition - 1 < 0 ? currentIndexes.length - 1 : currentPosition - 1
|
|
172
|
+
const newRowIndex = currentIndexes[newPosition][0]
|
|
173
|
+
const newId = currentIndexes[newPosition][1]
|
|
174
|
+
|
|
175
|
+
if (!isSelectionChangeAllowed(newId, newRowIndex)) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setSelectedRows(getNewSelectionState(newId, addToSelection))
|
|
180
|
+
scrollRowIntoView(newRowIndex)
|
|
181
|
+
},
|
|
182
|
+
[setSelectedRows, scrollRowIntoView, getNewSelectionState, isSelectionChangeAllowed],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Handle arrow key navigation with preventDefault to stop page scroll
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (keyboardNavigation === false) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
191
|
+
if (!isFocusWithinContainer() || isNull(cursorRef.current)) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const addToSelection = event.ctrlKey || event.metaKey || event.shiftKey
|
|
196
|
+
|
|
197
|
+
if (event.key === 'ArrowUp') {
|
|
198
|
+
event.preventDefault()
|
|
199
|
+
decreaseCursor(addToSelection)
|
|
200
|
+
} else if (event.key === 'ArrowDown') {
|
|
201
|
+
event.preventDefault()
|
|
202
|
+
increaseCursor(addToSelection)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
207
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
208
|
+
}, [keyboardNavigation, isFocusWithinContainer, increaseCursor, decreaseCursor])
|
|
209
|
+
|
|
210
|
+
// Handle Escape key via event listener to avoid state-based re-renders
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (keyboardNavigation === false) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
216
|
+
if (event.key !== 'Escape' || !isFocusWithinContainer()) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (editingCellRef.current) {
|
|
221
|
+
setEditingCell(null)
|
|
222
|
+
} else if (selectedRowsRef.current && isSelectionChangeAllowed(null, null)) {
|
|
223
|
+
setSelectedRows({ ids: [], id: null })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
window.addEventListener('keydown', handleEscapeKey)
|
|
228
|
+
return () => window.removeEventListener('keydown', handleEscapeKey)
|
|
229
|
+
}, [
|
|
230
|
+
keyboardNavigation,
|
|
231
|
+
isFocusWithinContainer,
|
|
232
|
+
setEditingCell,
|
|
233
|
+
setSelectedRows,
|
|
234
|
+
isSelectionChangeAllowed,
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
const setCursorOnRowSelectionChange = useCallback(() => {
|
|
238
|
+
if (selectedRows && selectedRows.id) {
|
|
239
|
+
// Find all rows with matching ID
|
|
240
|
+
const matchingRows = indexes.filter((item) => selectedRows.id === item[1])
|
|
241
|
+
|
|
242
|
+
if (matchingRows.length > 0) {
|
|
243
|
+
// If we have a current cursor, prefer the row at that position if it matches
|
|
244
|
+
if (!isNull(cursor)) {
|
|
245
|
+
const currentRow = indexes.find((item) => item[0] === cursor)
|
|
246
|
+
if (currentRow && currentRow[1] === selectedRows.id) {
|
|
247
|
+
// Current cursor position matches the selected ID, keep it
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Find the matching row closest to the current cursor position
|
|
252
|
+
const closest = matchingRows.reduce((prev, curr) => {
|
|
253
|
+
const prevDiff = Math.abs(prev[0] - cursor)
|
|
254
|
+
const currDiff = Math.abs(curr[0] - cursor)
|
|
255
|
+
return currDiff < prevDiff ? curr : prev
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (cursor !== closest[0]) {
|
|
259
|
+
setCursor(closest[0])
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// No cursor, use the first match
|
|
263
|
+
if (cursor !== matchingRows[0][0]) {
|
|
264
|
+
setCursor(matchingRows[0][0])
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
setCursor(null)
|
|
270
|
+
}
|
|
271
|
+
}, [cursor, selectedRows, indexes])
|
|
272
|
+
|
|
273
|
+
useEffect(setCursorOnRowSelectionChange, [selectedRows, indexes])
|
|
274
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isNumber, mapValues, reduce } from 'lodash-es'
|
|
2
|
+
import { Children, ReactElement, ReactNode, isValidElement } from 'react'
|
|
3
|
+
import { TableNode } from '@table-library/react-table-library/types/table'
|
|
4
|
+
import { isGroupHeader } from './is-group-header'
|
|
5
|
+
import { useTable } from './table-context'
|
|
6
|
+
|
|
7
|
+
interface ColumnProps {
|
|
8
|
+
sortKey?: string
|
|
9
|
+
sortFn?: (array: Array<TableNode>) => Array<TableNode>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HeaderProps {
|
|
13
|
+
children?: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useSortFunctions = (header: ReactElement<HeaderProps>, grouped?: boolean) => {
|
|
17
|
+
const sortFunctions: Record<number, (array: Array<TableNode>) => Array<TableNode>> = {}
|
|
18
|
+
const { select } = useTable()
|
|
19
|
+
|
|
20
|
+
const defaultSort = (sortKey: string) => {
|
|
21
|
+
return (array: Array<TableNode>) => {
|
|
22
|
+
// If an array of numbers then sort numerically.
|
|
23
|
+
if (isNumber(array[0][sortKey])) {
|
|
24
|
+
return array.sort((a, b) => a[sortKey] - b[sortKey])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Otherwise, sort alphabetically.
|
|
28
|
+
return array.sort((a, b) => {
|
|
29
|
+
if (a[sortKey] < b[sortKey]) {
|
|
30
|
+
return -1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (a[sortKey] > b[sortKey]) {
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 0
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Children.toArray(header.props.children).forEach((child, index) => {
|
|
43
|
+
if (!isValidElement<ColumnProps>(child)) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sortKey = child.props.sortKey
|
|
48
|
+
sortFunctions[select ? index + 1 : index] = !child.props.sortFn
|
|
49
|
+
? defaultSort(sortKey!)
|
|
50
|
+
: child.props.sortFn
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const groupSort = (sortFunction: (tableList: Array<TableNode>) => Array<TableNode>) => {
|
|
54
|
+
return (tableList: Array<TableNode>) => {
|
|
55
|
+
const splitLists = reduce(
|
|
56
|
+
tableList,
|
|
57
|
+
(acc, item) => {
|
|
58
|
+
if (isGroupHeader(item)) {
|
|
59
|
+
acc.push([])
|
|
60
|
+
} else {
|
|
61
|
+
acc[acc.length - 1].push(item)
|
|
62
|
+
}
|
|
63
|
+
return acc
|
|
64
|
+
},
|
|
65
|
+
[] as Array<Array<TableNode>>,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
splitLists.map((list) => (list.length ? sortFunction(list) : list))
|
|
69
|
+
|
|
70
|
+
const combineLists: Array<TableNode | Array<TableNode>> = []
|
|
71
|
+
const headers = tableList.filter((item) => isGroupHeader(item))
|
|
72
|
+
for (let i = 0; i < headers.length; i++) {
|
|
73
|
+
combineLists.push(headers[i])
|
|
74
|
+
combineLists.push(splitLists[i])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The library tolerates the nested sub-lists this produces; keep the
|
|
78
|
+
// vendored runtime shape and only assert the declared type.
|
|
79
|
+
return combineLists as Array<TableNode>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If table items are grouped, then split them by headers before sorting
|
|
84
|
+
// and then stitch back together after sorting.
|
|
85
|
+
if (grouped) {
|
|
86
|
+
return mapValues(sortFunctions, groupSort)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sortFunctions
|
|
90
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactElement } from 'react'
|
|
2
|
+
import { List, TabsListProps } from './list'
|
|
3
|
+
import { Tab, TabProps } from './tab'
|
|
4
|
+
import { Tabs as TabsRoot, TabsProps } from './tabs'
|
|
5
|
+
import { TabsSize, useTabs } from './tabs-context'
|
|
6
|
+
|
|
7
|
+
type TabsCompoundType = {
|
|
8
|
+
(props: TabsProps): ReactElement
|
|
9
|
+
List: typeof List
|
|
10
|
+
Tab: typeof Tab
|
|
11
|
+
useTabs: typeof useTabs
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Tabs: TabsCompoundType = Object.assign(TabsRoot, {
|
|
15
|
+
List,
|
|
16
|
+
Tab,
|
|
17
|
+
useTabs,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type { TabsProps, TabsListProps, TabProps, TabsSize }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
import { useTabs } from './tabs-context'
|
|
4
|
+
|
|
5
|
+
export type TabsListProps = {
|
|
6
|
+
className?: string
|
|
7
|
+
children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const List = ({ className, children }: TabsListProps) => {
|
|
11
|
+
const { size } = useTabs()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
role="tablist"
|
|
16
|
+
className={cn(
|
|
17
|
+
'flex flex-nowrap',
|
|
18
|
+
{
|
|
19
|
+
'items-start gap-6': size === 'lg',
|
|
20
|
+
'h-10 items-center gap-0.5 shrink-0 px-1.5 overflow-x-scroll hide-scrollbar':
|
|
21
|
+
size === 'md',
|
|
22
|
+
},
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
package/src/tabs/tab.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { HTMLAttributes, ReactNode, Ref } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
import { Link } from '../link'
|
|
4
|
+
import { useTabs } from './tabs-context'
|
|
5
|
+
|
|
6
|
+
export type TabProps = HTMLAttributes<HTMLButtonElement | HTMLAnchorElement> & {
|
|
7
|
+
value: string
|
|
8
|
+
className?: string
|
|
9
|
+
children: ReactNode
|
|
10
|
+
asLink?: boolean
|
|
11
|
+
href?: string
|
|
12
|
+
type?: 'button' | 'submit' | 'reset'
|
|
13
|
+
ref?: Ref<HTMLButtonElement | HTMLAnchorElement>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Tab = ({
|
|
17
|
+
value,
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
onClick,
|
|
21
|
+
asLink = false,
|
|
22
|
+
href,
|
|
23
|
+
type = 'button',
|
|
24
|
+
ref,
|
|
25
|
+
...restProps
|
|
26
|
+
}: TabProps) => {
|
|
27
|
+
const { value: selectedValue, onValueChange, size } = useTabs()
|
|
28
|
+
const isActive = value === selectedValue
|
|
29
|
+
|
|
30
|
+
const scrollToCenter = (el: HTMLElement) => {
|
|
31
|
+
el.scrollIntoView?.({
|
|
32
|
+
behavior: 'smooth',
|
|
33
|
+
inline: 'center',
|
|
34
|
+
block: 'nearest',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
39
|
+
onValueChange(value)
|
|
40
|
+
scrollToCenter(e.currentTarget)
|
|
41
|
+
onClick?.(e as React.MouseEvent<HTMLButtonElement & HTMLAnchorElement>)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleFocus = (e: React.FocusEvent<HTMLElement>) => {
|
|
45
|
+
if (e.currentTarget.matches(':focus-visible')) {
|
|
46
|
+
scrollToCenter(e.currentTarget)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sharedProps = {
|
|
51
|
+
role: 'tab' as const,
|
|
52
|
+
tabIndex: isActive ? -1 : 0,
|
|
53
|
+
'aria-selected': isActive,
|
|
54
|
+
onClick: handleClick,
|
|
55
|
+
onFocus: handleFocus,
|
|
56
|
+
...restProps,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (size === 'md') {
|
|
60
|
+
const tabClassName = cn(
|
|
61
|
+
'whitespace-nowrap text-xs text-gray-300 dark:text-zinc-400 font-medium select-none',
|
|
62
|
+
'rounded p-1 px-2 hover:text-gray-700 hover:bg-gray-50 dark:hover:text-zinc-100 dark:hover:bg-zinc-800',
|
|
63
|
+
'outline-none focus-visible:ring-2 ring-info/75',
|
|
64
|
+
{
|
|
65
|
+
'text-gray-700 bg-gray-50 dark:text-zinc-100 dark:bg-zinc-800': isActive,
|
|
66
|
+
},
|
|
67
|
+
className,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (asLink) {
|
|
71
|
+
return (
|
|
72
|
+
<Link
|
|
73
|
+
ref={ref as Ref<HTMLAnchorElement>}
|
|
74
|
+
href={href}
|
|
75
|
+
className={cn('text-inherit no-underline hover:no-underline', tabClassName)}
|
|
76
|
+
{...sharedProps}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</Link>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
ref={ref as Ref<HTMLButtonElement>}
|
|
86
|
+
type={type}
|
|
87
|
+
className={tabClassName}
|
|
88
|
+
{...sharedProps}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</button>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const wrapperClassName = cn(
|
|
96
|
+
'-mb-px rounded py-2 px-0 text-sm font-bold text-gray-300 dark:text-zinc-400 text-center',
|
|
97
|
+
'whitespace-nowrap',
|
|
98
|
+
{
|
|
99
|
+
'rounded-b-none border-b-4 border-info text-gray-500 dark:text-zinc-100': isActive,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const innerClassName = cn(
|
|
104
|
+
'rounded-xl px-3 py-1 outline-none',
|
|
105
|
+
'focus-visible:ring-2 focus-visible:ring-info/75',
|
|
106
|
+
'transition-all duration-200 ease-in-out',
|
|
107
|
+
{
|
|
108
|
+
'hover:text-gray-500 hover:bg-gray-100/50 dark:hover:text-zinc-100 dark:hover:bg-zinc-800/50':
|
|
109
|
+
!isActive,
|
|
110
|
+
},
|
|
111
|
+
className,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if (asLink) {
|
|
115
|
+
return (
|
|
116
|
+
<div data-wrapper className={wrapperClassName}>
|
|
117
|
+
<Link
|
|
118
|
+
ref={ref as Ref<HTMLAnchorElement>}
|
|
119
|
+
href={href}
|
|
120
|
+
className={cn('text-inherit no-underline hover:no-underline', innerClassName)}
|
|
121
|
+
{...sharedProps}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</Link>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div data-wrapper className={wrapperClassName}>
|
|
131
|
+
<button
|
|
132
|
+
ref={ref as Ref<HTMLButtonElement>}
|
|
133
|
+
type={type}
|
|
134
|
+
className={innerClassName}
|
|
135
|
+
{...sharedProps}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { createContext, ReactNode, useContext } from 'react'
|
|
4
|
+
|
|
5
|
+
export type TabsSize = 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
export interface TabsContextValue {
|
|
8
|
+
value: string
|
|
9
|
+
onValueChange: (value: string) => void
|
|
10
|
+
size: TabsSize
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TabsContext = createContext<TabsContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
export const useTabs = () => {
|
|
16
|
+
const context = useContext(TabsContext)
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('Tabs.Tab must be used within a Tabs component')
|
|
19
|
+
}
|
|
20
|
+
return context
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TabsProviderProps {
|
|
24
|
+
children: ReactNode
|
|
25
|
+
value: string
|
|
26
|
+
onValueChange: (value: string) => void
|
|
27
|
+
size: TabsSize
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const TabsProvider = ({ children, value, onValueChange, size }: TabsProviderProps) => {
|
|
31
|
+
const contextValue: TabsContextValue = {
|
|
32
|
+
value,
|
|
33
|
+
onValueChange,
|
|
34
|
+
size,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <TabsContext.Provider value={contextValue}>{children}</TabsContext.Provider>
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
import { TabsProvider, TabsSize } from './tabs-context'
|
|
4
|
+
|
|
5
|
+
export type TabsProps = {
|
|
6
|
+
value: string
|
|
7
|
+
onValueChange: (value: string) => void
|
|
8
|
+
size?: TabsSize
|
|
9
|
+
className?: string
|
|
10
|
+
children: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Tabs = ({ value, onValueChange, size = 'lg', className, children }: TabsProps) => (
|
|
14
|
+
<TabsProvider value={value} onValueChange={onValueChange} size={size}>
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
'border-b border-gray-100 dark:border-zinc-800',
|
|
18
|
+
{
|
|
19
|
+
'overflow-x-auto overflow-y-hidden hide-scrollbar': size === 'lg',
|
|
20
|
+
},
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
</TabsProvider>
|
|
27
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './textarea'
|