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.
Files changed (45) hide show
  1. package/README.md +218 -0
  2. package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  3. package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  4. package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  5. package/client-dist/assets/index-BeRNeRUq.css +1 -0
  6. package/client-dist/assets/index-uoZ4c_I8.js +164 -0
  7. package/client-dist/index.html +13 -0
  8. package/index.html +12 -0
  9. package/package.json +55 -0
  10. package/src/client/App.tsx +885 -0
  11. package/src/client/components/connection-status.tsx +43 -0
  12. package/src/client/components/data-table-cell.tsx +235 -0
  13. package/src/client/components/data-table-col-icon.tsx +73 -0
  14. package/src/client/components/data-table-header-col.tsx +225 -0
  15. package/src/client/components/data-table-search-input.tsx +729 -0
  16. package/src/client/components/data-table.tsx +2014 -0
  17. package/src/client/components/stream-controls.tsx +157 -0
  18. package/src/client/components/theme-provider.tsx +230 -0
  19. package/src/client/components/ui/button.tsx +68 -0
  20. package/src/client/components/ui/combobox.tsx +308 -0
  21. package/src/client/components/ui/context-menu.tsx +261 -0
  22. package/src/client/components/ui/dropdown-menu.tsx +267 -0
  23. package/src/client/components/ui/input-group.tsx +153 -0
  24. package/src/client/components/ui/input.tsx +19 -0
  25. package/src/client/components/ui/textarea.tsx +18 -0
  26. package/src/client/components/viewer-settings.tsx +185 -0
  27. package/src/client/index.css +192 -0
  28. package/src/client/lib/data-table-search.ts +750 -0
  29. package/src/client/lib/datool-icons.ts +37 -0
  30. package/src/client/lib/datool-url-state.ts +159 -0
  31. package/src/client/lib/filterable-table.ts +146 -0
  32. package/src/client/lib/table-search-persistence.ts +94 -0
  33. package/src/client/lib/utils.ts +6 -0
  34. package/src/client/main.tsx +14 -0
  35. package/src/index.ts +19 -0
  36. package/src/node/cli.ts +54 -0
  37. package/src/node/config.ts +231 -0
  38. package/src/node/lines.ts +82 -0
  39. package/src/node/runtime.ts +102 -0
  40. package/src/node/server.ts +403 -0
  41. package/src/node/sources/command.ts +82 -0
  42. package/src/node/sources/file.ts +116 -0
  43. package/src/node/sources/ssh.ts +59 -0
  44. package/src/shared/columns.ts +41 -0
  45. package/src/shared/types.ts +188 -0
@@ -0,0 +1,267 @@
1
+ import * as React from "react"
2
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { CheckIcon, ChevronRightIcon } from "lucide-react"
6
+
7
+ function DropdownMenu({
8
+ ...props
9
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
10
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
11
+ }
12
+
13
+ function DropdownMenuPortal({
14
+ ...props
15
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
16
+ return (
17
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
18
+ )
19
+ }
20
+
21
+ function DropdownMenuTrigger({
22
+ ...props
23
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
24
+ return (
25
+ <DropdownMenuPrimitive.Trigger
26
+ data-slot="dropdown-menu-trigger"
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function DropdownMenuContent({
33
+ className,
34
+ align = "start",
35
+ sideOffset = 4,
36
+ ...props
37
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
38
+ return (
39
+ <DropdownMenuPrimitive.Portal>
40
+ <DropdownMenuPrimitive.Content
41
+ data-slot="dropdown-menu-content"
42
+ sideOffset={sideOffset}
43
+ align={align}
44
+ className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
45
+ {...props}
46
+ />
47
+ </DropdownMenuPrimitive.Portal>
48
+ )
49
+ }
50
+
51
+ function DropdownMenuGroup({
52
+ ...props
53
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
54
+ return (
55
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
56
+ )
57
+ }
58
+
59
+ function DropdownMenuItem({
60
+ className,
61
+ inset,
62
+ variant = "default",
63
+ ...props
64
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
65
+ inset?: boolean
66
+ variant?: "default" | "destructive"
67
+ }) {
68
+ return (
69
+ <DropdownMenuPrimitive.Item
70
+ data-slot="dropdown-menu-item"
71
+ data-inset={inset}
72
+ data-variant={variant}
73
+ className={cn(
74
+ "group/dropdown-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function DropdownMenuCheckboxItem({
83
+ className,
84
+ children,
85
+ checked,
86
+ inset,
87
+ ...props
88
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
89
+ inset?: boolean
90
+ }) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ data-inset={inset}
95
+ className={cn(
96
+ "relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
97
+ className
98
+ )}
99
+ checked={checked}
100
+ {...props}
101
+ >
102
+ <span
103
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
104
+ data-slot="dropdown-menu-checkbox-item-indicator"
105
+ >
106
+ <DropdownMenuPrimitive.ItemIndicator>
107
+ <CheckIcon
108
+ />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ )
114
+ }
115
+
116
+ function DropdownMenuRadioGroup({
117
+ ...props
118
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
119
+ return (
120
+ <DropdownMenuPrimitive.RadioGroup
121
+ data-slot="dropdown-menu-radio-group"
122
+ {...props}
123
+ />
124
+ )
125
+ }
126
+
127
+ function DropdownMenuRadioItem({
128
+ className,
129
+ children,
130
+ inset,
131
+ ...props
132
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
133
+ inset?: boolean
134
+ }) {
135
+ return (
136
+ <DropdownMenuPrimitive.RadioItem
137
+ data-slot="dropdown-menu-radio-item"
138
+ data-inset={inset}
139
+ className={cn(
140
+ "relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
141
+ className
142
+ )}
143
+ {...props}
144
+ >
145
+ <span
146
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
147
+ data-slot="dropdown-menu-radio-item-indicator"
148
+ >
149
+ <DropdownMenuPrimitive.ItemIndicator>
150
+ <CheckIcon
151
+ />
152
+ </DropdownMenuPrimitive.ItemIndicator>
153
+ </span>
154
+ {children}
155
+ </DropdownMenuPrimitive.RadioItem>
156
+ )
157
+ }
158
+
159
+ function DropdownMenuLabel({
160
+ className,
161
+ inset,
162
+ ...props
163
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
164
+ inset?: boolean
165
+ }) {
166
+ return (
167
+ <DropdownMenuPrimitive.Label
168
+ data-slot="dropdown-menu-label"
169
+ data-inset={inset}
170
+ className={cn(
171
+ "px-2 py-1.5 text-xs text-muted-foreground data-inset:pl-7.5",
172
+ className
173
+ )}
174
+ {...props}
175
+ />
176
+ )
177
+ }
178
+
179
+ function DropdownMenuSeparator({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
183
+ return (
184
+ <DropdownMenuPrimitive.Separator
185
+ data-slot="dropdown-menu-separator"
186
+ className={cn("-mx-1 my-1 h-px bg-border/50", className)}
187
+ {...props}
188
+ />
189
+ )
190
+ }
191
+
192
+ function DropdownMenuShortcut({
193
+ className,
194
+ ...props
195
+ }: React.ComponentProps<"span">) {
196
+ return (
197
+ <span
198
+ data-slot="dropdown-menu-shortcut"
199
+ className={cn(
200
+ "ml-auto text-[0.625rem] tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
201
+ className
202
+ )}
203
+ {...props}
204
+ />
205
+ )
206
+ }
207
+
208
+ function DropdownMenuSub({
209
+ ...props
210
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
211
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
212
+ }
213
+
214
+ function DropdownMenuSubTrigger({
215
+ className,
216
+ inset,
217
+ children,
218
+ ...props
219
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
220
+ inset?: boolean
221
+ }) {
222
+ return (
223
+ <DropdownMenuPrimitive.SubTrigger
224
+ data-slot="dropdown-menu-sub-trigger"
225
+ data-inset={inset}
226
+ className={cn(
227
+ "flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
228
+ className
229
+ )}
230
+ {...props}
231
+ >
232
+ {children}
233
+ <ChevronRightIcon className="ml-auto" />
234
+ </DropdownMenuPrimitive.SubTrigger>
235
+ )
236
+ }
237
+
238
+ function DropdownMenuSubContent({
239
+ className,
240
+ ...props
241
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
242
+ return (
243
+ <DropdownMenuPrimitive.SubContent
244
+ data-slot="dropdown-menu-sub-content"
245
+ className={cn("z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
246
+ {...props}
247
+ />
248
+ )
249
+ }
250
+
251
+ export {
252
+ DropdownMenu,
253
+ DropdownMenuPortal,
254
+ DropdownMenuTrigger,
255
+ DropdownMenuContent,
256
+ DropdownMenuGroup,
257
+ DropdownMenuLabel,
258
+ DropdownMenuItem,
259
+ DropdownMenuCheckboxItem,
260
+ DropdownMenuRadioGroup,
261
+ DropdownMenuRadioItem,
262
+ DropdownMenuSeparator,
263
+ DropdownMenuShortcut,
264
+ DropdownMenuSub,
265
+ DropdownMenuSubTrigger,
266
+ DropdownMenuSubContent,
267
+ }
@@ -0,0 +1,153 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Textarea } from "@/components/ui/textarea"
8
+
9
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10
+ return (
11
+ <div
12
+ data-slot="input-group"
13
+ role="group"
14
+ className={cn(
15
+ "group/input-group relative flex h-7 w-full min-w-0 items-center rounded-md border border-input bg-input/20 transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ const inputGroupAddonVariants = cva(
24
+ "flex h-auto cursor-text items-center justify-center gap-1 py-2 text-xs/relaxed font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-[calc(var(--radius-sm)-2px)] **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:text-[0.625rem] [&>svg:not([class*='size-'])]:size-3.5",
25
+ {
26
+ variants: {
27
+ align: {
28
+ "inline-start":
29
+ "order-first pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem]",
30
+ "inline-end":
31
+ "order-last pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem]",
32
+ "block-start":
33
+ "order-first w-full justify-start px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
34
+ "block-end":
35
+ "order-last w-full justify-start px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ align: "inline-start",
40
+ },
41
+ }
42
+ )
43
+
44
+ function InputGroupAddon({
45
+ className,
46
+ align = "inline-start",
47
+ ...props
48
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
49
+ return (
50
+ <div
51
+ role="group"
52
+ data-slot="input-group-addon"
53
+ data-align={align}
54
+ className={cn(inputGroupAddonVariants({ align }), className)}
55
+ onClick={(e) => {
56
+ if ((e.target as HTMLElement).closest("button")) {
57
+ return
58
+ }
59
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
60
+ }}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ const inputGroupButtonVariants = cva(
67
+ "flex items-center gap-2 rounded-md text-xs/relaxed shadow-none",
68
+ {
69
+ variants: {
70
+ size: {
71
+ xs: "h-5 gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1 [&>svg:not([class*='size-'])]:size-3",
72
+ sm: "gap-1",
73
+ "icon-xs": "size-6 p-0 has-[>svg]:p-0",
74
+ "icon-sm": "size-7 p-0 has-[>svg]:p-0",
75
+ },
76
+ },
77
+ defaultVariants: {
78
+ size: "xs",
79
+ },
80
+ }
81
+ )
82
+
83
+ function InputGroupButton({
84
+ className,
85
+ type = "button",
86
+ variant = "ghost",
87
+ size = "xs",
88
+ ...props
89
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
90
+ VariantProps<typeof inputGroupButtonVariants>) {
91
+ return (
92
+ <Button
93
+ type={type}
94
+ data-size={size}
95
+ variant={variant}
96
+ className={cn(inputGroupButtonVariants({ size }), className)}
97
+ {...props}
98
+ />
99
+ )
100
+ }
101
+
102
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
103
+ return (
104
+ <span
105
+ className={cn(
106
+ "flex items-center gap-2 text-xs/relaxed text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
107
+ className
108
+ )}
109
+ {...props}
110
+ />
111
+ )
112
+ }
113
+
114
+ function InputGroupInput({
115
+ className,
116
+ ...props
117
+ }: React.ComponentProps<"input">) {
118
+ return (
119
+ <Input
120
+ data-slot="input-group-control"
121
+ className={cn(
122
+ "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
123
+ className
124
+ )}
125
+ {...props}
126
+ />
127
+ )
128
+ }
129
+
130
+ function InputGroupTextarea({
131
+ className,
132
+ ...props
133
+ }: React.ComponentProps<"textarea">) {
134
+ return (
135
+ <Textarea
136
+ data-slot="input-group-control"
137
+ className={cn(
138
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
139
+ className
140
+ )}
141
+ {...props}
142
+ />
143
+ )
144
+ }
145
+
146
+ export {
147
+ InputGroup,
148
+ InputGroupAddon,
149
+ InputGroupButton,
150
+ InputGroupText,
151
+ InputGroupInput,
152
+ InputGroupTextarea,
153
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "flex field-sizing-content min-h-16 w-full resize-none rounded-md border border-input bg-input/20 px-2 py-2 text-sm transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }
@@ -0,0 +1,185 @@
1
+ import * as React from "react"
2
+ import {
3
+ DownloadIcon,
4
+ EllipsisIcon,
5
+ MoonIcon,
6
+ Settings2Icon,
7
+ SunIcon,
8
+ } from "lucide-react"
9
+
10
+ import {
11
+ DataTableColIcon,
12
+ type DataTableColumnKind,
13
+ } from "./data-table-col-icon"
14
+ import { type Theme, useTheme } from "./theme-provider"
15
+ import { Button } from "@/components/ui/button"
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuCheckboxItem,
19
+ DropdownMenuContent,
20
+ DropdownMenuItem,
21
+ DropdownMenuLabel,
22
+ DropdownMenuRadioGroup,
23
+ DropdownMenuRadioItem,
24
+ DropdownMenuSeparator,
25
+ DropdownMenuSub,
26
+ DropdownMenuSubContent,
27
+ DropdownMenuSubTrigger,
28
+ DropdownMenuTrigger,
29
+ } from "@/components/ui/dropdown-menu"
30
+ import { cn } from "@/lib/utils"
31
+
32
+ type ViewerSettingsColumn = {
33
+ id: string
34
+ kind?: DataTableColumnKind
35
+ label: string
36
+ visible: boolean
37
+ }
38
+
39
+ type ViewerSettingsProps = {
40
+ columns: ViewerSettingsColumn[]
41
+ isDisabled?: boolean
42
+ onExportCsv: () => void
43
+ onExportMarkdown: () => void
44
+ onToggleColumn: (columnId: string, visible: boolean) => void
45
+ className?: string
46
+ }
47
+
48
+ const THEME_OPTIONS: Array<{
49
+ icon: React.ComponentType<{ className?: string }>
50
+ label: string
51
+ value: Theme
52
+ }> = [
53
+ {
54
+ icon: SunIcon,
55
+ label: "Light",
56
+ value: "light",
57
+ },
58
+ {
59
+ icon: MoonIcon,
60
+ label: "Dark",
61
+ value: "dark",
62
+ },
63
+ {
64
+ icon: Settings2Icon,
65
+ label: "System",
66
+ value: "system",
67
+ },
68
+ ]
69
+
70
+ export function ViewerSettings({
71
+ columns,
72
+ isDisabled = false,
73
+ onExportCsv,
74
+ onExportMarkdown,
75
+ onToggleColumn,
76
+ className,
77
+ }: ViewerSettingsProps) {
78
+ const { theme, setTheme } = useTheme()
79
+ const canExport = !isDisabled && columns.length > 0
80
+
81
+ return (
82
+ <DropdownMenu>
83
+ <DropdownMenuTrigger asChild>
84
+ <Button
85
+ type="button"
86
+ variant="outline"
87
+ size="icon-xl"
88
+ disabled={isDisabled}
89
+ className={cn("shrink-0", className)}
90
+ aria-label="Open settings"
91
+ >
92
+ <EllipsisIcon className="size-4" />
93
+ </Button>
94
+ </DropdownMenuTrigger>
95
+ <DropdownMenuContent
96
+ align="end"
97
+ sideOffset={10}
98
+ className="w-64 max-w-[min(24rem,calc(100vw-2rem))]"
99
+ >
100
+ <DropdownMenuLabel>View</DropdownMenuLabel>
101
+ <DropdownMenuSub>
102
+ <DropdownMenuSubTrigger className="min-h-9 text-sm">
103
+ <Settings2Icon className="size-4 text-muted-foreground" />
104
+ Columns
105
+ </DropdownMenuSubTrigger>
106
+ <DropdownMenuSubContent className="max-h-80 w-64 overflow-y-auto">
107
+ <DropdownMenuLabel>Visible columns</DropdownMenuLabel>
108
+ <DropdownMenuSeparator />
109
+ {columns.map((column) => (
110
+ <DropdownMenuCheckboxItem
111
+ key={column.id}
112
+ checked={column.visible}
113
+ className="min-h-9 text-sm"
114
+ onSelect={(event) => {
115
+ event.preventDefault()
116
+ }}
117
+ onCheckedChange={(checked) =>
118
+ onToggleColumn(column.id, checked === true)
119
+ }
120
+ >
121
+ <span className="flex min-w-0 items-center gap-2 pr-4">
122
+ {column.kind ? (
123
+ <DataTableColIcon
124
+ kind={column.kind}
125
+ className="size-4 shrink-0 text-muted-foreground"
126
+ />
127
+ ) : null}
128
+ <span className="truncate">{column.label}</span>
129
+ </span>
130
+ </DropdownMenuCheckboxItem>
131
+ ))}
132
+ </DropdownMenuSubContent>
133
+ </DropdownMenuSub>
134
+ <DropdownMenuSub>
135
+ <DropdownMenuSubTrigger className="min-h-9 text-sm">
136
+ <SunIcon className="size-4 text-muted-foreground dark:hidden" />
137
+ <MoonIcon className="hidden size-4 text-muted-foreground dark:block" />
138
+ Theme
139
+ </DropdownMenuSubTrigger>
140
+ <DropdownMenuSubContent className="w-44">
141
+ <DropdownMenuLabel>Appearance</DropdownMenuLabel>
142
+ <DropdownMenuSeparator />
143
+ <DropdownMenuRadioGroup
144
+ value={theme}
145
+ onValueChange={(value) => setTheme(value as Theme)}
146
+ >
147
+ {THEME_OPTIONS.map((option) => {
148
+ const Icon = option.icon
149
+
150
+ return (
151
+ <DropdownMenuRadioItem
152
+ key={option.value}
153
+ value={option.value}
154
+ className="min-h-9 text-sm"
155
+ >
156
+ <Icon className="size-4 text-muted-foreground" />
157
+ {option.label}
158
+ </DropdownMenuRadioItem>
159
+ )
160
+ })}
161
+ </DropdownMenuRadioGroup>
162
+ </DropdownMenuSubContent>
163
+ </DropdownMenuSub>
164
+ <DropdownMenuSeparator />
165
+ <DropdownMenuLabel>Export</DropdownMenuLabel>
166
+ <DropdownMenuItem
167
+ className="min-h-9 text-sm"
168
+ disabled={!canExport}
169
+ onSelect={onExportCsv}
170
+ >
171
+ <DownloadIcon className="size-4 text-muted-foreground" />
172
+ Export CSV
173
+ </DropdownMenuItem>
174
+ <DropdownMenuItem
175
+ className="min-h-9 text-sm"
176
+ disabled={!canExport}
177
+ onSelect={onExportMarkdown}
178
+ >
179
+ <DownloadIcon className="size-4 text-muted-foreground" />
180
+ Export Markdown
181
+ </DropdownMenuItem>
182
+ </DropdownMenuContent>
183
+ </DropdownMenu>
184
+ )
185
+ }