datool 0.0.1
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/README.md +218 -0
- package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/client-dist/assets/index-BeRNeRUq.css +1 -0
- package/client-dist/assets/index-uoZ4c_I8.js +164 -0
- package/client-dist/index.html +13 -0
- package/index.html +12 -0
- package/package.json +55 -0
- package/src/client/App.tsx +885 -0
- package/src/client/components/connection-status.tsx +43 -0
- package/src/client/components/data-table-cell.tsx +235 -0
- package/src/client/components/data-table-col-icon.tsx +73 -0
- package/src/client/components/data-table-header-col.tsx +225 -0
- package/src/client/components/data-table-search-input.tsx +729 -0
- package/src/client/components/data-table.tsx +2014 -0
- package/src/client/components/stream-controls.tsx +157 -0
- package/src/client/components/theme-provider.tsx +230 -0
- package/src/client/components/ui/button.tsx +68 -0
- package/src/client/components/ui/combobox.tsx +308 -0
- package/src/client/components/ui/context-menu.tsx +261 -0
- package/src/client/components/ui/dropdown-menu.tsx +267 -0
- package/src/client/components/ui/input-group.tsx +153 -0
- package/src/client/components/ui/input.tsx +19 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/viewer-settings.tsx +185 -0
- package/src/client/index.css +192 -0
- package/src/client/lib/data-table-search.ts +750 -0
- package/src/client/lib/datool-icons.ts +37 -0
- package/src/client/lib/datool-url-state.ts +159 -0
- package/src/client/lib/filterable-table.ts +146 -0
- package/src/client/lib/table-search-persistence.ts +94 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +14 -0
- package/src/index.ts +19 -0
- package/src/node/cli.ts +54 -0
- package/src/node/config.ts +231 -0
- package/src/node/lines.ts +82 -0
- package/src/node/runtime.ts +102 -0
- package/src/node/server.ts +403 -0
- package/src/node/sources/command.ts +82 -0
- package/src/node/sources/file.ts +116 -0
- package/src/node/sources/ssh.ts +59 -0
- package/src/shared/columns.ts +41 -0
- package/src/shared/types.ts +188 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
type ConnectionStatusProps = {
|
|
4
|
+
isConnected: boolean
|
|
5
|
+
isConnecting: boolean
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ConnectionStatus({
|
|
10
|
+
isConnected,
|
|
11
|
+
isConnecting,
|
|
12
|
+
className,
|
|
13
|
+
}: ConnectionStatusProps) {
|
|
14
|
+
const status = isConnected
|
|
15
|
+
? {
|
|
16
|
+
dotClassName: "bg-emerald-500",
|
|
17
|
+
label: "Connected",
|
|
18
|
+
textClassName: "text-emerald-600 dark:text-emerald-400",
|
|
19
|
+
}
|
|
20
|
+
: isConnecting
|
|
21
|
+
? {
|
|
22
|
+
dotClassName: "bg-amber-500",
|
|
23
|
+
label: "Connecting",
|
|
24
|
+
textClassName: "text-amber-600 dark:text-amber-400",
|
|
25
|
+
}
|
|
26
|
+
: {
|
|
27
|
+
dotClassName: "bg-muted-foreground/60",
|
|
28
|
+
label: "Disconnected",
|
|
29
|
+
textClassName: "text-muted-foreground",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={cn("inline-flex items-center gap-2 text-xs font-medium", className)}
|
|
35
|
+
>
|
|
36
|
+
<span
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
className={cn("size-2 rounded-full", status.dotClassName)}
|
|
39
|
+
/>
|
|
40
|
+
<span className={status.textClassName}>{status.label}</span>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
import { flexRender, type Cell } from "@tanstack/react-table"
|
|
3
|
+
import { Check, Minus } from "lucide-react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import type { DataTableColumnKind } from "./data-table-col-icon"
|
|
7
|
+
import type { DataTableColumnMeta } from "./data-table-header-col"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
|
|
10
|
+
function getAlignmentClassName(align: DataTableColumnMeta["align"] = "left") {
|
|
11
|
+
switch (align) {
|
|
12
|
+
case "center":
|
|
13
|
+
return "justify-center text-center"
|
|
14
|
+
case "right":
|
|
15
|
+
return "justify-end text-right"
|
|
16
|
+
default:
|
|
17
|
+
return "justify-start text-left"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatDate(value: string | number | Date) {
|
|
22
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
23
|
+
|
|
24
|
+
if (Number.isNaN(date.getTime())) {
|
|
25
|
+
return String(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
29
|
+
dateStyle: "medium",
|
|
30
|
+
}).format(date)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatNumber(value: number) {
|
|
34
|
+
return new Intl.NumberFormat(undefined, {
|
|
35
|
+
maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
36
|
+
}).format(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fallbackCellValue(value: unknown, kind?: DataTableColumnKind) {
|
|
40
|
+
if (value === null || value === undefined || value === "") {
|
|
41
|
+
return <span className="text-muted-foreground">-</span>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (kind === "boolean" || typeof value === "boolean") {
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
className={cn(
|
|
48
|
+
"inline-flex min-w-16 items-center justify-center rounded-full border px-2 py-1 text-[11px] font-medium",
|
|
49
|
+
value
|
|
50
|
+
? "border-border bg-accent text-accent-foreground"
|
|
51
|
+
: "border-border bg-muted text-muted-foreground"
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{value ? "True" : "False"}
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (kind === "number" || typeof value === "number") {
|
|
60
|
+
return formatNumber(Number(value))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (kind === "date" || value instanceof Date) {
|
|
64
|
+
return formatDate(value as string | number | Date)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value) || typeof value === "object") {
|
|
68
|
+
return (
|
|
69
|
+
<code className="rounded bg-muted px-1.5 py-1 font-mono text-[11px] text-muted-foreground">
|
|
70
|
+
{JSON.stringify(value)}
|
|
71
|
+
</code>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return String(value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderHighlightedTextParts(value: string, terms: string[]) {
|
|
79
|
+
const normalizedTerms = Array.from(
|
|
80
|
+
new Set(terms.map((term) => term.trim()).filter(Boolean))
|
|
81
|
+
).sort((left, right) => right.length - left.length)
|
|
82
|
+
|
|
83
|
+
if (normalizedTerms.length === 0) {
|
|
84
|
+
return [value]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pattern = new RegExp(
|
|
88
|
+
`(${normalizedTerms
|
|
89
|
+
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
90
|
+
.join("|")})`,
|
|
91
|
+
"gi"
|
|
92
|
+
)
|
|
93
|
+
const parts = value.split(pattern)
|
|
94
|
+
|
|
95
|
+
return parts.map((part, index) => {
|
|
96
|
+
const matched = normalizedTerms.some(
|
|
97
|
+
(term) => part.toLowerCase() === term.toLowerCase()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (!matched) {
|
|
101
|
+
return <React.Fragment key={`${part}-${index}`}>{part}</React.Fragment>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<mark
|
|
106
|
+
className="rounded-sm bg-primary/20 ring-2 ring-primary/20 dark:bg-primary/50 dark:text-white dark:ring-primary/80"
|
|
107
|
+
key={`${part}-${index}`}
|
|
108
|
+
>
|
|
109
|
+
{part}
|
|
110
|
+
</mark>
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderHighlightedText(
|
|
116
|
+
value: string,
|
|
117
|
+
terms: string[],
|
|
118
|
+
rendered: React.ReactNode
|
|
119
|
+
) {
|
|
120
|
+
const highlightedParts = renderHighlightedTextParts(value, terms)
|
|
121
|
+
|
|
122
|
+
if (React.isValidElement(rendered)) {
|
|
123
|
+
const element = rendered as React.ReactElement<{
|
|
124
|
+
children?: React.ReactNode
|
|
125
|
+
}>
|
|
126
|
+
|
|
127
|
+
if (typeof element.props.children === "string") {
|
|
128
|
+
return React.cloneElement(element, undefined, highlightedParts)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return <span className="whitespace-pre-wrap">{highlightedParts}</span>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function DataTableCheckbox({
|
|
136
|
+
checked,
|
|
137
|
+
indeterminate,
|
|
138
|
+
onCheckedChange,
|
|
139
|
+
ariaLabel,
|
|
140
|
+
}: {
|
|
141
|
+
checked: boolean
|
|
142
|
+
indeterminate?: boolean
|
|
143
|
+
onCheckedChange: (checked: boolean) => void
|
|
144
|
+
ariaLabel: string
|
|
145
|
+
}) {
|
|
146
|
+
const ref = React.useRef<HTMLInputElement>(null)
|
|
147
|
+
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (!ref.current) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ref.current.indeterminate = Boolean(indeterminate) && !checked
|
|
154
|
+
}, [checked, indeterminate])
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<label className="inline-flex cursor-pointer items-center justify-center">
|
|
158
|
+
<input
|
|
159
|
+
ref={ref}
|
|
160
|
+
aria-label={ariaLabel}
|
|
161
|
+
checked={checked}
|
|
162
|
+
className="peer sr-only"
|
|
163
|
+
onChange={(event) => onCheckedChange(event.target.checked)}
|
|
164
|
+
type="checkbox"
|
|
165
|
+
/>
|
|
166
|
+
<span
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex size-5 items-center justify-center rounded-[6px] border border-border bg-background text-primary-foreground shadow-xs transition-colors peer-focus-visible:ring-2 peer-focus-visible:ring-ring/50",
|
|
169
|
+
(checked || indeterminate) && "border-primary bg-primary"
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{indeterminate && !checked ? <Minus className="size-3.5" /> : null}
|
|
173
|
+
{checked ? <Check className="size-3.5" /> : null}
|
|
174
|
+
</span>
|
|
175
|
+
</label>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function DataTableBodyCell<TData>({
|
|
180
|
+
cell,
|
|
181
|
+
highlightTerms = [],
|
|
182
|
+
paddingLeft,
|
|
183
|
+
paddingRight,
|
|
184
|
+
}: {
|
|
185
|
+
cell: Cell<TData, unknown>
|
|
186
|
+
highlightTerms?: string[]
|
|
187
|
+
paddingLeft?: React.CSSProperties["paddingLeft"]
|
|
188
|
+
paddingRight?: React.CSSProperties["paddingRight"]
|
|
189
|
+
}) {
|
|
190
|
+
const meta = (cell.column.columnDef.meta ?? {}) as DataTableColumnMeta
|
|
191
|
+
const rawValue = cell.getValue()
|
|
192
|
+
const rendered = flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
193
|
+
const isSticky = meta.sticky === "left"
|
|
194
|
+
const isSelectionCell = meta.kind === "selection"
|
|
195
|
+
const shouldTruncate = meta.truncate ?? true
|
|
196
|
+
const shouldHighlight =
|
|
197
|
+
meta.kind === "text" &&
|
|
198
|
+
meta.highlightMatches !== false &&
|
|
199
|
+
typeof rawValue === "string" &&
|
|
200
|
+
highlightTerms.length > 0
|
|
201
|
+
const content = shouldHighlight
|
|
202
|
+
? renderHighlightedText(rawValue, highlightTerms, rendered)
|
|
203
|
+
: (rendered ?? fallbackCellValue(rawValue, meta.kind))
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<td
|
|
207
|
+
data-sticky-cell={isSticky ? "true" : "false"}
|
|
208
|
+
className={cn(
|
|
209
|
+
"flex shrink-0 border-b border-border px-2 py-1.5 align-middle text-sm text-foreground",
|
|
210
|
+
getAlignmentClassName(meta.align),
|
|
211
|
+
isSticky && "sticky left-0 z-10 border-r border-r-border bg-card",
|
|
212
|
+
meta.cellClassName
|
|
213
|
+
)}
|
|
214
|
+
style={{
|
|
215
|
+
paddingLeft,
|
|
216
|
+
paddingRight,
|
|
217
|
+
width: cell.column.getSize(),
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
className={cn(
|
|
222
|
+
isSelectionCell
|
|
223
|
+
? "flex w-full items-center justify-center"
|
|
224
|
+
: shouldTruncate
|
|
225
|
+
? "min-w-0 truncate"
|
|
226
|
+
: "w-full min-w-0 break-words whitespace-normal"
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{content}
|
|
230
|
+
</div>
|
|
231
|
+
</td>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { fallbackCellValue }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
import {
|
|
3
|
+
Braces,
|
|
4
|
+
CalendarDays,
|
|
5
|
+
CheckSquare2,
|
|
6
|
+
Hash,
|
|
7
|
+
ListFilter,
|
|
8
|
+
Type,
|
|
9
|
+
} from "lucide-react"
|
|
10
|
+
import type { LucideProps } from "lucide-react"
|
|
11
|
+
import type { ComponentType } from "react"
|
|
12
|
+
|
|
13
|
+
export type DataTableColumnKind =
|
|
14
|
+
| "text"
|
|
15
|
+
| "enum"
|
|
16
|
+
| "number"
|
|
17
|
+
| "boolean"
|
|
18
|
+
| "date"
|
|
19
|
+
| "json"
|
|
20
|
+
| "selection"
|
|
21
|
+
|
|
22
|
+
const iconMap: Record<DataTableColumnKind, ComponentType<LucideProps>> = {
|
|
23
|
+
text: Type,
|
|
24
|
+
enum: ListFilter,
|
|
25
|
+
number: Hash,
|
|
26
|
+
boolean: CheckSquare2,
|
|
27
|
+
date: CalendarDays,
|
|
28
|
+
json: Braces,
|
|
29
|
+
selection: CheckSquare2,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DataTableColIcon({
|
|
33
|
+
kind,
|
|
34
|
+
...props
|
|
35
|
+
}: { kind: DataTableColumnKind } & LucideProps) {
|
|
36
|
+
const Icon = iconMap[kind]
|
|
37
|
+
|
|
38
|
+
return <Icon {...props} />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isDateString(value: string) {
|
|
42
|
+
if (!/\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return !Number.isNaN(Date.parse(value))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function inferDataTableColumnKind(values: unknown[]) {
|
|
50
|
+
const sample = values.find((value) => value !== null && value !== undefined)
|
|
51
|
+
|
|
52
|
+
if (sample instanceof Date) {
|
|
53
|
+
return "date" as const
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof sample === "boolean") {
|
|
57
|
+
return "boolean" as const
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof sample === "number") {
|
|
61
|
+
return "number" as const
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof sample === "string" && isDateString(sample)) {
|
|
65
|
+
return "date" as const
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sample && typeof sample === "object") {
|
|
69
|
+
return "json" as const
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return "text" as const
|
|
73
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { flexRender, type Header } from "@tanstack/react-table"
|
|
2
|
+
import { ArrowDown, ArrowUp, Search } from "lucide-react"
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { createPortal } from "react-dom"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DataTableColIcon,
|
|
8
|
+
type DataTableColumnKind,
|
|
9
|
+
} from "./data-table-col-icon"
|
|
10
|
+
import { cn } from "@/lib/utils"
|
|
11
|
+
|
|
12
|
+
export type DataTableAlign = "left" | "center" | "right"
|
|
13
|
+
|
|
14
|
+
export type DataTableColumnMeta = {
|
|
15
|
+
align?: DataTableAlign
|
|
16
|
+
cellClassName?: string
|
|
17
|
+
headerClassName?: string
|
|
18
|
+
highlightMatches?: boolean
|
|
19
|
+
kind?: DataTableColumnKind
|
|
20
|
+
sticky?: "left"
|
|
21
|
+
truncate?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getAlignmentClassName(align: DataTableAlign = "left") {
|
|
25
|
+
switch (align) {
|
|
26
|
+
case "center":
|
|
27
|
+
return "justify-center text-center"
|
|
28
|
+
case "right":
|
|
29
|
+
return "justify-end text-right"
|
|
30
|
+
default:
|
|
31
|
+
return "justify-start text-left"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function HeaderSortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
|
|
36
|
+
if (sorted === "asc") {
|
|
37
|
+
return <ArrowUp className="size-3.5 text-foreground" />
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sorted === "desc") {
|
|
41
|
+
return <ArrowDown className="size-3.5 text-foreground" />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function DataTableHeaderCol<TData>({
|
|
48
|
+
header,
|
|
49
|
+
highlightEnabled = false,
|
|
50
|
+
onToggleHighlight,
|
|
51
|
+
paddingLeft,
|
|
52
|
+
paddingRight,
|
|
53
|
+
scrollContainerRef,
|
|
54
|
+
}: {
|
|
55
|
+
header: Header<TData, unknown>
|
|
56
|
+
highlightEnabled?: boolean
|
|
57
|
+
onToggleHighlight?: () => void
|
|
58
|
+
paddingLeft?: React.CSSProperties["paddingLeft"]
|
|
59
|
+
paddingRight?: React.CSSProperties["paddingRight"]
|
|
60
|
+
scrollContainerRef: React.RefObject<HTMLDivElement | null>
|
|
61
|
+
}) {
|
|
62
|
+
const meta = (header.column.columnDef.meta ?? {}) as DataTableColumnMeta
|
|
63
|
+
const sorted = header.column.getIsSorted()
|
|
64
|
+
const canSort = header.column.getCanSort()
|
|
65
|
+
const alignmentClassName = getAlignmentClassName(meta.align)
|
|
66
|
+
const isSticky = meta.sticky === "left"
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<th
|
|
70
|
+
className={cn(
|
|
71
|
+
"relative flex shrink-0 border-b border-gray-300 bg-background py-2 align-middle text-xs font-medium tracking-wide text-muted-foreground uppercase dark:border-border",
|
|
72
|
+
isSticky && "sticky left-0 z-20 border-r border-r-border bg-background",
|
|
73
|
+
meta.headerClassName
|
|
74
|
+
)}
|
|
75
|
+
style={{
|
|
76
|
+
width: header.getSize(),
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{header.isPlaceholder ? null : canSort ? (
|
|
80
|
+
<div className="flex w-full min-w-0 items-center gap-2">
|
|
81
|
+
<button
|
|
82
|
+
className={cn(
|
|
83
|
+
"flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 py-1 transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none",
|
|
84
|
+
alignmentClassName
|
|
85
|
+
)}
|
|
86
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
87
|
+
style={{
|
|
88
|
+
paddingLeft,
|
|
89
|
+
paddingRight,
|
|
90
|
+
}}
|
|
91
|
+
type="button"
|
|
92
|
+
>
|
|
93
|
+
{meta.kind && meta.kind !== "selection" ? (
|
|
94
|
+
<DataTableColIcon
|
|
95
|
+
className="size-3.5 shrink-0 text-muted-foreground"
|
|
96
|
+
kind={meta.kind}
|
|
97
|
+
/>
|
|
98
|
+
) : null}
|
|
99
|
+
<span className="truncate font-medium text-foreground normal-case">
|
|
100
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
101
|
+
</span>
|
|
102
|
+
<HeaderSortIcon sorted={sorted} />
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<div
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex w-full min-w-0 items-center gap-1 px-0.5 py-1",
|
|
109
|
+
alignmentClassName
|
|
110
|
+
)}
|
|
111
|
+
style={{
|
|
112
|
+
paddingLeft,
|
|
113
|
+
paddingRight,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{meta.kind && meta.kind !== "selection" ? (
|
|
117
|
+
<DataTableColIcon
|
|
118
|
+
className="size-3.5 shrink-0 text-muted-foreground"
|
|
119
|
+
kind={meta.kind}
|
|
120
|
+
/>
|
|
121
|
+
) : null}
|
|
122
|
+
<span className="truncate font-medium text-foreground normal-case">
|
|
123
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
124
|
+
</span>
|
|
125
|
+
{meta.kind === "text" && onToggleHighlight ? (
|
|
126
|
+
<button
|
|
127
|
+
className={cn(
|
|
128
|
+
"inline-flex size-5 items-center justify-center rounded-sm border",
|
|
129
|
+
highlightEnabled
|
|
130
|
+
? "border-border bg-accent text-accent-foreground"
|
|
131
|
+
: "border-transparent text-muted-foreground"
|
|
132
|
+
)}
|
|
133
|
+
onClick={onToggleHighlight}
|
|
134
|
+
type="button"
|
|
135
|
+
>
|
|
136
|
+
<Search className="size-3" />
|
|
137
|
+
</button>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
{header.column.getCanResize() ? (
|
|
142
|
+
<ResizeHandler
|
|
143
|
+
header={header}
|
|
144
|
+
scrollContainerRef={scrollContainerRef}
|
|
145
|
+
/>
|
|
146
|
+
) : null}
|
|
147
|
+
</th>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ResizeHandler<TData>({
|
|
152
|
+
header,
|
|
153
|
+
scrollContainerRef,
|
|
154
|
+
}: {
|
|
155
|
+
header: Header<TData, unknown>
|
|
156
|
+
scrollContainerRef: React.RefObject<HTMLDivElement | null>
|
|
157
|
+
}) {
|
|
158
|
+
const isResizing = header.column.getIsResizing()
|
|
159
|
+
const handleRef = React.useRef<HTMLDivElement>(null)
|
|
160
|
+
const [overlayBounds, setOverlayBounds] = React.useState<{
|
|
161
|
+
bottom: number
|
|
162
|
+
left: number
|
|
163
|
+
top: number
|
|
164
|
+
} | null>(null)
|
|
165
|
+
const transform = isResizing
|
|
166
|
+
? `translateX(${
|
|
167
|
+
header.getContext().table.getState().columnSizingInfo.deltaOffset ?? 0
|
|
168
|
+
}px)`
|
|
169
|
+
: ""
|
|
170
|
+
|
|
171
|
+
React.useLayoutEffect(() => {
|
|
172
|
+
if (!isResizing || !handleRef.current || !scrollContainerRef.current) {
|
|
173
|
+
setOverlayBounds(null)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const updateBounds = () => {
|
|
178
|
+
if (!handleRef.current || !scrollContainerRef.current) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handleBounds = handleRef.current.getBoundingClientRect()
|
|
183
|
+
const containerBounds = scrollContainerRef.current.getBoundingClientRect()
|
|
184
|
+
|
|
185
|
+
setOverlayBounds({
|
|
186
|
+
bottom: Math.max(window.innerHeight - containerBounds.bottom, 0),
|
|
187
|
+
left: handleBounds.left,
|
|
188
|
+
top: containerBounds.top,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
updateBounds()
|
|
193
|
+
window.addEventListener("resize", updateBounds)
|
|
194
|
+
|
|
195
|
+
return () => window.removeEventListener("resize", updateBounds)
|
|
196
|
+
}, [isResizing, scrollContainerRef])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<>
|
|
200
|
+
{isResizing && overlayBounds
|
|
201
|
+
? createPortal(
|
|
202
|
+
<div
|
|
203
|
+
className="pointer-events-none fixed z-[100] w-1 animate-in cursor-col-resize bg-border transition-colors duration-75 zoom-in-50"
|
|
204
|
+
style={{
|
|
205
|
+
bottom: overlayBounds.bottom,
|
|
206
|
+
left: overlayBounds.left,
|
|
207
|
+
top: overlayBounds.top,
|
|
208
|
+
transform,
|
|
209
|
+
}}
|
|
210
|
+
/>,
|
|
211
|
+
document.body
|
|
212
|
+
)
|
|
213
|
+
: null}
|
|
214
|
+
<div
|
|
215
|
+
ref={handleRef}
|
|
216
|
+
className={cn(
|
|
217
|
+
"absolute top-0 right-[-2px] z-[100] h-full w-1 cursor-col-resize touch-none rounded-full bg-border opacity-0 transition-opacity select-none hover:opacity-100",
|
|
218
|
+
isResizing && "opacity-100"
|
|
219
|
+
)}
|
|
220
|
+
onMouseDown={header.getResizeHandler()}
|
|
221
|
+
onTouchStart={header.getResizeHandler()}
|
|
222
|
+
/>
|
|
223
|
+
</>
|
|
224
|
+
)
|
|
225
|
+
}
|