@mostrom/app-shell 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 (142) hide show
  1. package/.claude/ralph-loop.local.md +9 -0
  2. package/README.md +172 -0
  3. package/bin/init.js +269 -0
  4. package/bun.lock +401 -0
  5. package/components.json +28 -0
  6. package/package.json +74 -0
  7. package/scripts/publish-npm.sh +202 -0
  8. package/src/AppShell.tsx +847 -0
  9. package/src/components/PageHeader.tsx +160 -0
  10. package/src/components/data-table/README.md +447 -0
  11. package/src/components/data-table/data-table-preferences.tsx +184 -0
  12. package/src/components/data-table/data-table-toolbar.tsx +118 -0
  13. package/src/components/data-table/data-table.tsx +37 -0
  14. package/src/components/data-table/index.ts +32 -0
  15. package/src/components/global-header/AllServicesButton.tsx +127 -0
  16. package/src/components/global-header/CategoriesButton.tsx +120 -0
  17. package/src/components/global-header/GlobalHeader.tsx +59 -0
  18. package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
  19. package/src/components/global-header/HeaderUtilities.tsx +243 -0
  20. package/src/components/global-header/ServicesMenu.tsx +246 -0
  21. package/src/components/layout/AppBreadcrumb.tsx +70 -0
  22. package/src/components/layout/AppFlashbar.tsx +95 -0
  23. package/src/components/layout/AppLayout.tsx +271 -0
  24. package/src/components/layout/AppNavigation.tsx +313 -0
  25. package/src/components/layout/AppSidebar.tsx +229 -0
  26. package/src/components/patterns/index.ts +14 -0
  27. package/src/components/patterns/p-alert-5.tsx +19 -0
  28. package/src/components/patterns/p-autocomplete-5.tsx +89 -0
  29. package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
  30. package/src/components/patterns/p-button-42.tsx +37 -0
  31. package/src/components/patterns/p-button-51.tsx +14 -0
  32. package/src/components/patterns/p-button-6.tsx +5 -0
  33. package/src/components/patterns/p-calendar-1.tsx +18 -0
  34. package/src/components/patterns/p-card-1.tsx +33 -0
  35. package/src/components/patterns/p-card-2.tsx +26 -0
  36. package/src/components/patterns/p-card-5.tsx +31 -0
  37. package/src/components/patterns/p-collapsible-7.tsx +121 -0
  38. package/src/components/patterns/p-command-6.tsx +113 -0
  39. package/src/components/patterns/p-dialog-1.tsx +56 -0
  40. package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
  41. package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
  42. package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
  43. package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
  44. package/src/components/patterns/p-empty-2.tsx +34 -0
  45. package/src/components/patterns/p-file-upload-1.tsx +72 -0
  46. package/src/components/patterns/p-filters-1.tsx +666 -0
  47. package/src/components/patterns/p-frame-2.tsx +26 -0
  48. package/src/components/patterns/p-tabs-2.tsx +129 -0
  49. package/src/components/reui/alert.tsx +92 -0
  50. package/src/components/reui/autocomplete.tsx +343 -0
  51. package/src/components/reui/badge.tsx +87 -0
  52. package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
  53. package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
  54. package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
  55. package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
  56. package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
  57. package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
  58. package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
  59. package/src/components/reui/data-grid/data-grid.tsx +209 -0
  60. package/src/components/reui/date-selector.tsx +1330 -0
  61. package/src/components/reui/filters.tsx +1869 -0
  62. package/src/components/reui/frame.tsx +134 -0
  63. package/src/components/reui/index.ts +17 -0
  64. package/src/components/reui/timeline.tsx +219 -0
  65. package/src/components/search/Autocomplete.tsx +183 -0
  66. package/src/components/search/AutocompleteClient.tsx +293 -0
  67. package/src/components/search/GlobalSearch.tsx +187 -0
  68. package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
  69. package/src/components/section-drawer/index.ts +19 -0
  70. package/src/components/section-drawer/section-drawer.css +665 -0
  71. package/src/components/section-drawer/section-drawer.tsx +467 -0
  72. package/src/components/sectioned-list-board/README.md +78 -0
  73. package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
  74. package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
  75. package/src/components/sectioned-list-board/index.ts +19 -0
  76. package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
  77. package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
  78. package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
  79. package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
  80. package/src/components/sectioned-list-board/types.ts +216 -0
  81. package/src/components/sectioned-list-table/README.md +80 -0
  82. package/src/components/sectioned-list-table/index.ts +14 -0
  83. package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
  84. package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
  85. package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
  86. package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
  87. package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
  88. package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
  89. package/src/components/sectioned-list-table/types.ts +120 -0
  90. package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
  91. package/src/components/ui/actions-dropdown.tsx +109 -0
  92. package/src/components/ui/assignee-selector.tsx +209 -0
  93. package/src/components/ui/avatar.tsx +107 -0
  94. package/src/components/ui/breadcrumb.tsx +109 -0
  95. package/src/components/ui/button-group.tsx +83 -0
  96. package/src/components/ui/button.tsx +64 -0
  97. package/src/components/ui/calendar.tsx +220 -0
  98. package/src/components/ui/card.tsx +92 -0
  99. package/src/components/ui/chart.tsx +376 -0
  100. package/src/components/ui/checkbox.tsx +30 -0
  101. package/src/components/ui/collapsible.tsx +33 -0
  102. package/src/components/ui/command.tsx +182 -0
  103. package/src/components/ui/context-menu.tsx +250 -0
  104. package/src/components/ui/create-button-group.tsx +128 -0
  105. package/src/components/ui/dialog.tsx +156 -0
  106. package/src/components/ui/drawer.tsx +133 -0
  107. package/src/components/ui/dropdown-menu.tsx +255 -0
  108. package/src/components/ui/empty.tsx +104 -0
  109. package/src/components/ui/field.tsx +248 -0
  110. package/src/components/ui/form.tsx +165 -0
  111. package/src/components/ui/index.ts +37 -0
  112. package/src/components/ui/input-group.tsx +168 -0
  113. package/src/components/ui/input.tsx +21 -0
  114. package/src/components/ui/kbd.tsx +28 -0
  115. package/src/components/ui/label.tsx +22 -0
  116. package/src/components/ui/navigation-menu.tsx +168 -0
  117. package/src/components/ui/page-header.tsx +80 -0
  118. package/src/components/ui/popover.tsx +87 -0
  119. package/src/components/ui/scroll-area.tsx +56 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +26 -0
  122. package/src/components/ui/sheet.tsx +141 -0
  123. package/src/components/ui/sidebar.tsx +726 -0
  124. package/src/components/ui/skeleton.tsx +13 -0
  125. package/src/components/ui/sonner.tsx +38 -0
  126. package/src/components/ui/switch.tsx +33 -0
  127. package/src/components/ui/tabs.tsx +91 -0
  128. package/src/components/ui/textarea.tsx +18 -0
  129. package/src/components/ui/toggle-group.tsx +83 -0
  130. package/src/components/ui/toggle.tsx +45 -0
  131. package/src/components/ui/tooltip.tsx +57 -0
  132. package/src/hooks/use-copy-to-clipboard.ts +37 -0
  133. package/src/hooks/use-file-upload.ts +415 -0
  134. package/src/hooks/use-mobile.ts +19 -0
  135. package/src/index.ts +95 -0
  136. package/src/lib/utils.ts +6 -0
  137. package/src/styles.css +1859 -0
  138. package/src/urls.ts +83 -0
  139. package/src/vite.d.ts +22 -0
  140. package/src/vite.js +241 -0
  141. package/tsconfig.base.json +18 -0
  142. package/tsconfig.json +24 -0
@@ -0,0 +1,165 @@
1
+ "use client"
2
+
3
+ import { useMemo, useState } from "react"
4
+ import { Column } from "@tanstack/react-table"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Badge } from "@/components/reui/badge"
8
+ import { Button } from "@/components/ui/button"
9
+ import { Input } from "@/components/ui/input"
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "@/components/ui/popover"
15
+ import { Separator } from "@/components/ui/separator"
16
+ import { CirclePlusIcon, CheckIcon } from "lucide-react"
17
+
18
+ interface DataGridColumnFilterProps<TData, TValue> {
19
+ column?: Column<TData, TValue>
20
+ title?: string
21
+ options: {
22
+ label: string
23
+ value: string
24
+ icon?: React.ComponentType<{ className?: string }>
25
+ }[]
26
+ }
27
+
28
+ function DataGridColumnFilter<TData, TValue>({
29
+ column,
30
+ title,
31
+ options,
32
+ }: DataGridColumnFilterProps<TData, TValue>) {
33
+ const facets = column?.getFacetedUniqueValues()
34
+ const selectedValues = new Set(column?.getFilterValue() as string[])
35
+ const [searchQuery, setSearchQuery] = useState("")
36
+
37
+ const filteredOptions = useMemo(() => {
38
+ if (!searchQuery) return options
39
+ return options.filter((option) =>
40
+ option.label.toLowerCase().includes(searchQuery.toLowerCase())
41
+ )
42
+ }, [options, searchQuery])
43
+
44
+ return (
45
+ <Popover>
46
+ <PopoverTrigger asChild>
47
+ <Button variant="outline" size="sm">
48
+ <CirclePlusIcon className="size-4" />
49
+ {title}
50
+ {selectedValues?.size > 0 && (
51
+ <>
52
+ <Separator orientation="vertical" className="mx-2 h-4" />
53
+ <Badge
54
+ variant="secondary"
55
+ className="rounded-sm px-1 font-normal lg:hidden"
56
+ >
57
+ {selectedValues.size}
58
+ </Badge>
59
+ <div className="hidden space-x-1 lg:flex">
60
+ {selectedValues.size > 2 ? (
61
+ <Badge
62
+ variant="secondary"
63
+ className="rounded-sm px-1 font-normal"
64
+ >
65
+ {selectedValues.size} selected
66
+ </Badge>
67
+ ) : (
68
+ options
69
+ .filter((option) => selectedValues.has(option.value))
70
+ .map((option) => (
71
+ <Badge
72
+ variant="secondary"
73
+ key={option.value}
74
+ className="rounded-sm px-1 font-normal"
75
+ >
76
+ {option.label}
77
+ </Badge>
78
+ ))
79
+ )}
80
+ </div>
81
+ </>
82
+ )}
83
+ </Button>
84
+ </PopoverTrigger>
85
+ <PopoverContent className="w-[200px] p-0" align="start">
86
+ <div className="p-2">
87
+ <Input
88
+ placeholder={title}
89
+ value={searchQuery}
90
+ onChange={(e) => setSearchQuery(e.target.value)}
91
+ className="h-8"
92
+ />
93
+ </div>
94
+ <div className="max-h-[300px] overflow-y-auto">
95
+ {filteredOptions.length === 0 ? (
96
+ <div className="text-muted-foreground py-6 text-center text-sm">
97
+ No results found.
98
+ </div>
99
+ ) : (
100
+ <div className="p-1">
101
+ {filteredOptions.map((option) => {
102
+ const isSelected = selectedValues.has(option.value)
103
+ return (
104
+ <div
105
+ key={option.value}
106
+ onClick={() => {
107
+ if (isSelected) {
108
+ selectedValues.delete(option.value)
109
+ } else {
110
+ selectedValues.add(option.value)
111
+ }
112
+ const filterValues = Array.from(selectedValues)
113
+ column?.setFilterValue(
114
+ filterValues.length ? filterValues : undefined
115
+ )
116
+ }}
117
+ className={cn(
118
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
119
+ "hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
120
+ )}
121
+ >
122
+ <div
123
+ className={cn(
124
+ "border-primary me-2 flex h-4 w-4 items-center justify-center rounded-sm border",
125
+ isSelected
126
+ ? "bg-primary text-primary-foreground"
127
+ : "opacity-50 [&_svg]:invisible"
128
+ )}
129
+ >
130
+ <CheckIcon className="size-4" />
131
+ </div>
132
+ {option.icon && (
133
+ <option.icon className="text-muted-foreground mr-2 h-4 w-4" />
134
+ )}
135
+ <span>{option.label}</span>
136
+ {facets?.get(option.value) && (
137
+ <span className="ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
138
+ {facets.get(option.value)}
139
+ </span>
140
+ )}
141
+ </div>
142
+ )
143
+ })}
144
+ </div>
145
+ )}
146
+ {selectedValues.size > 0 && (
147
+ <>
148
+ <div className="bg-border -mx-1 my-1 h-px" />
149
+ <div className="p-1">
150
+ <div
151
+ onClick={() => column?.setFilterValue(undefined)}
152
+ className="hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center justify-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none"
153
+ >
154
+ Clear filters
155
+ </div>
156
+ </div>
157
+ </>
158
+ )}
159
+ </div>
160
+ </PopoverContent>
161
+ </Popover>
162
+ )
163
+ }
164
+
165
+ export { DataGridColumnFilter, type DataGridColumnFilterProps }
@@ -0,0 +1,339 @@
1
+ import { HTMLAttributes, memo, ReactNode, useMemo } from "react"
2
+ import { useDataGrid } from "@/components/reui/data-grid/data-grid"
3
+ import { Column } from "@tanstack/react-table"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { Button } from "@/components/ui/button"
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuCheckboxItem,
10
+ DropdownMenuContent,
11
+ DropdownMenuGroup,
12
+ DropdownMenuItem,
13
+ DropdownMenuLabel,
14
+ DropdownMenuSeparator,
15
+ DropdownMenuSub,
16
+ DropdownMenuSubContent,
17
+ DropdownMenuSubTrigger,
18
+ DropdownMenuTrigger,
19
+ } from "@/components/ui/dropdown-menu"
20
+ import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react"
21
+
22
+ interface DataGridColumnHeaderProps<
23
+ TData,
24
+ TValue,
25
+ > extends HTMLAttributes<HTMLDivElement> {
26
+ column: Column<TData, TValue>
27
+ title?: string
28
+ icon?: ReactNode
29
+ pinnable?: boolean
30
+ filter?: ReactNode
31
+ visibility?: boolean
32
+ }
33
+
34
+ function DataGridColumnHeaderInner<TData, TValue>({
35
+ column,
36
+ title = "",
37
+ icon,
38
+ className,
39
+ filter,
40
+ visibility = false,
41
+ }: DataGridColumnHeaderProps<TData, TValue>) {
42
+ const { isLoading, table, props, recordCount } = useDataGrid()
43
+
44
+ const columnOrder = table.getState().columnOrder
45
+ const columnVisibilityKey = JSON.stringify(table.getState().columnVisibility)
46
+ const isSorted = column.getIsSorted()
47
+ const isPinned = column.getIsPinned()
48
+ const canSort = column.getCanSort()
49
+ const canPin = column.getCanPin()
50
+ const canResize = column.getCanResize()
51
+
52
+ const columnIndex = columnOrder.indexOf(column.id)
53
+ const canMoveLeft = columnIndex > 0
54
+ const canMoveRight = columnIndex < columnOrder.length - 1
55
+
56
+ const handleSort = () => {
57
+ if (isSorted === "asc") {
58
+ column.toggleSorting(true)
59
+ } else if (isSorted === "desc") {
60
+ column.clearSorting()
61
+ } else {
62
+ column.toggleSorting(false)
63
+ }
64
+ }
65
+
66
+ const headerLabelClassName = cn(
67
+ "text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5",
68
+ className
69
+ )
70
+
71
+ const headerButtonClassName = cn(
72
+ "text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground px-0 has-[>svg]:px-0 justify-start font-normal h-6 rounded-lg",
73
+ className
74
+ )
75
+
76
+ const sortIcon =
77
+ canSort &&
78
+ (isSorted === "desc" ? (
79
+ <ArrowDownIcon className="size-3.25" />
80
+ ) : isSorted === "asc" ? (
81
+ <ArrowUpIcon className="size-3.25" />
82
+ ) : (
83
+ <ChevronsUpDownIcon className="mt-px size-3.25" />
84
+ ))
85
+
86
+ const hasControls =
87
+ props.tableLayout?.columnsMovable ||
88
+ (props.tableLayout?.columnsVisibility && visibility) ||
89
+ (props.tableLayout?.columnsPinnable && canPin) ||
90
+ filter
91
+
92
+ const menuItems = useMemo(() => {
93
+ const items: ReactNode[] = []
94
+ let hasPreviousSection = false
95
+
96
+ // Filter section
97
+ if (filter) {
98
+ items.push(
99
+ <DropdownMenuGroup key="group-filter">
100
+ <DropdownMenuLabel key="filter">{filter}</DropdownMenuLabel>
101
+ </DropdownMenuGroup>
102
+ )
103
+ hasPreviousSection = true
104
+ }
105
+
106
+ // Sort section
107
+ if (canSort) {
108
+ if (hasPreviousSection) {
109
+ items.push(<DropdownMenuSeparator key="sep-sort" />)
110
+ }
111
+ items.push(
112
+ <DropdownMenuItem
113
+ key="sort-asc"
114
+ onClick={() => {
115
+ if (isSorted === "asc") {
116
+ column.clearSorting()
117
+ } else {
118
+ column.toggleSorting(false)
119
+ }
120
+ }}
121
+ disabled={!canSort}
122
+ >
123
+ <ArrowUpIcon className="size-3.5!" />
124
+ <span className="grow">Asc</span>
125
+ {isSorted === "asc" && (
126
+ <CheckIcon className="text-primary size-4 opacity-100!" />
127
+ )}
128
+ </DropdownMenuItem>,
129
+ <DropdownMenuItem
130
+ key="sort-desc"
131
+ onClick={() => {
132
+ if (isSorted === "desc") {
133
+ column.clearSorting()
134
+ } else {
135
+ column.toggleSorting(true)
136
+ }
137
+ }}
138
+ disabled={!canSort}
139
+ >
140
+ <ArrowDownIcon className="size-3.5!" />
141
+ <span className="grow">Desc</span>
142
+ {isSorted === "desc" && (
143
+ <CheckIcon className="text-primary size-4 opacity-100!" />
144
+ )}
145
+ </DropdownMenuItem>
146
+ )
147
+ hasPreviousSection = true
148
+ }
149
+
150
+ // Pin section
151
+ if (props.tableLayout?.columnsPinnable && canPin) {
152
+ if (hasPreviousSection) {
153
+ items.push(<DropdownMenuSeparator key="sep-pin" />)
154
+ }
155
+ items.push(
156
+ <DropdownMenuItem
157
+ key="pin-left"
158
+ onClick={() => column.pin(isPinned === "left" ? false : "left")}
159
+ >
160
+ <ArrowLeftToLineIcon className="size-3.5!" aria-hidden="true" />
161
+ <span className="grow">Pin to left</span>
162
+ {isPinned === "left" && (
163
+ <CheckIcon className="text-primary size-4 opacity-100!" />
164
+ )}
165
+ </DropdownMenuItem>,
166
+ <DropdownMenuItem
167
+ key="pin-right"
168
+ onClick={() => column.pin(isPinned === "right" ? false : "right")}
169
+ >
170
+ <ArrowRightToLineIcon className="size-3.5!" aria-hidden="true" />
171
+ <span className="grow">Pin to right</span>
172
+ {isPinned === "right" && (
173
+ <CheckIcon className="text-primary size-4 opacity-100!" />
174
+ )}
175
+ </DropdownMenuItem>
176
+ )
177
+ hasPreviousSection = true
178
+ }
179
+
180
+ // Move section
181
+ if (props.tableLayout?.columnsMovable) {
182
+ if (hasPreviousSection) {
183
+ items.push(<DropdownMenuSeparator key="sep-move" />)
184
+ }
185
+ items.push(
186
+ <DropdownMenuItem
187
+ key="move-left"
188
+ onClick={() => {
189
+ if (columnIndex > 0) {
190
+ const newOrder = [...columnOrder]
191
+ const [movedColumn] = newOrder.splice(columnIndex, 1)
192
+ newOrder.splice(columnIndex - 1, 0, movedColumn)
193
+ table.setColumnOrder(newOrder)
194
+ }
195
+ }}
196
+ disabled={!canMoveLeft || isPinned !== false}
197
+ >
198
+ <ArrowLeftIcon className="size-3.5!" aria-hidden="true" />
199
+ <span>Move to Left</span>
200
+ </DropdownMenuItem>,
201
+ <DropdownMenuItem
202
+ key="move-right"
203
+ onClick={() => {
204
+ if (columnIndex < columnOrder.length - 1) {
205
+ const newOrder = [...columnOrder]
206
+ const [movedColumn] = newOrder.splice(columnIndex, 1)
207
+ newOrder.splice(columnIndex + 1, 0, movedColumn)
208
+ table.setColumnOrder(newOrder)
209
+ }
210
+ }}
211
+ disabled={!canMoveRight || isPinned !== false}
212
+ >
213
+ <ArrowRightIcon className="size-3.5!" aria-hidden="true" />
214
+ <span>Move to Right</span>
215
+ </DropdownMenuItem>
216
+ )
217
+ hasPreviousSection = true
218
+ }
219
+
220
+ // Visibility section
221
+ if (props.tableLayout?.columnsVisibility && visibility) {
222
+ if (hasPreviousSection) {
223
+ items.push(<DropdownMenuSeparator key="sep-visibility" />)
224
+ }
225
+ items.push(
226
+ <DropdownMenuSub key="visibility">
227
+ <DropdownMenuSubTrigger>
228
+ <Settings2Icon className="size-3.5!" />
229
+ <span>Columns</span>
230
+ </DropdownMenuSubTrigger>
231
+ <DropdownMenuSubContent>
232
+ {table
233
+ .getAllColumns()
234
+ .filter(
235
+ (col) =>
236
+ typeof col.accessorFn !== "undefined" && col.getCanHide()
237
+ )
238
+ .map((col) => (
239
+ <DropdownMenuCheckboxItem
240
+ key={col.id}
241
+ checked={col.getIsVisible()}
242
+ onSelect={(event) => event.preventDefault()}
243
+ onCheckedChange={(value) => col.toggleVisibility(!!value)}
244
+ className="capitalize"
245
+ >
246
+ {col.columnDef.meta?.headerTitle || col.id}
247
+ </DropdownMenuCheckboxItem>
248
+ ))}
249
+ </DropdownMenuSubContent>
250
+ </DropdownMenuSub>
251
+ )
252
+ }
253
+
254
+ return items
255
+ // eslint-disable-next-line react-hooks/exhaustive-deps
256
+ }, [
257
+ filter,
258
+ canSort,
259
+ isSorted,
260
+ column,
261
+ props.tableLayout?.columnsPinnable,
262
+ props.tableLayout?.columnsMovable,
263
+ props.tableLayout?.columnsVisibility,
264
+ canPin,
265
+ isPinned,
266
+ canMoveLeft,
267
+ canMoveRight,
268
+ visibility,
269
+ table,
270
+ columnIndex,
271
+ columnOrder,
272
+ columnVisibilityKey, // Needed to update checkbox states when visibility changes
273
+ ])
274
+
275
+ if (hasControls) {
276
+ return (
277
+ <div className="flex h-full items-center justify-between gap-1.5">
278
+ <DropdownMenu>
279
+ <DropdownMenuTrigger asChild>
280
+ <Button
281
+ variant="ghost"
282
+ className={headerButtonClassName}
283
+ disabled={isLoading || recordCount === 0}
284
+ >
285
+ {icon && icon}
286
+ {title}
287
+ {sortIcon}
288
+ </Button>
289
+ </DropdownMenuTrigger>
290
+ <DropdownMenuContent className="w-40" align="start">
291
+ {menuItems}
292
+ </DropdownMenuContent>
293
+ </DropdownMenu>
294
+ {props.tableLayout?.columnsPinnable && canPin && isPinned && (
295
+ <Button
296
+ size="icon-sm"
297
+ variant="ghost"
298
+ className="-me-1 size-7 rounded-md"
299
+ onClick={() => column.pin(false)}
300
+ aria-label={`Unpin ${title} column`}
301
+ title={`Unpin ${title} column`}
302
+ >
303
+ <PinOffIcon className="size-3.5! opacity-50!" aria-hidden="true" />
304
+ </Button>
305
+ )}
306
+ </div>
307
+ )
308
+ }
309
+
310
+ if (canSort || (props.tableLayout?.columnsResizable && canResize)) {
311
+ return (
312
+ <div className="flex h-full items-center">
313
+ <Button
314
+ variant="ghost"
315
+ className={headerButtonClassName}
316
+ disabled={isLoading || recordCount === 0}
317
+ onClick={handleSort}
318
+ >
319
+ {icon && icon}
320
+ {title}
321
+ {sortIcon}
322
+ </Button>
323
+ </div>
324
+ )
325
+ }
326
+
327
+ return (
328
+ <div className={headerLabelClassName}>
329
+ {icon && icon}
330
+ {title}
331
+ </div>
332
+ )
333
+ }
334
+
335
+ const DataGridColumnHeader = memo(
336
+ DataGridColumnHeaderInner
337
+ ) as typeof DataGridColumnHeaderInner
338
+
339
+ export { DataGridColumnHeader, type DataGridColumnHeaderProps }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import { ReactElement } from "react"
4
+ import { Table } from "@tanstack/react-table"
5
+
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuCheckboxItem,
9
+ DropdownMenuContent,
10
+ DropdownMenuGroup,
11
+ DropdownMenuLabel,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+
15
+ function DataGridColumnVisibility<TData>({
16
+ table,
17
+ trigger,
18
+ }: {
19
+ table: Table<TData>
20
+ trigger: ReactElement<Record<string, unknown>>
21
+ }) {
22
+ return (
23
+ <DropdownMenu>
24
+ <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
25
+ <DropdownMenuContent align="end" className="min-w-[150px]">
26
+ <DropdownMenuGroup>
27
+ <DropdownMenuLabel className="font-medium">
28
+ Toggle Columns
29
+ </DropdownMenuLabel>
30
+ {table
31
+ .getAllColumns()
32
+ .filter(
33
+ (column) =>
34
+ typeof column.accessorFn !== "undefined" && column.getCanHide()
35
+ )
36
+ .map((column) => {
37
+ return (
38
+ <DropdownMenuCheckboxItem
39
+ key={column.id}
40
+ className="capitalize"
41
+ checked={column.getIsVisible()}
42
+ onSelect={(event) => event.preventDefault()}
43
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
44
+ >
45
+ {column.columnDef.meta?.headerTitle || column.id}
46
+ </DropdownMenuCheckboxItem>
47
+ )
48
+ })}
49
+ </DropdownMenuGroup>
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ )
53
+ }
54
+
55
+ export { DataGridColumnVisibility }