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,157 @@
1
+ import * as React from "react"
2
+ import {
3
+ ChevronDownIcon,
4
+ LoaderCircleIcon,
5
+ PlayCircleIcon,
6
+ StopCircleIcon,
7
+ XIcon,
8
+ } from "lucide-react"
9
+
10
+ import { Button } from "@/components/ui/button"
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuLabel,
15
+ DropdownMenuRadioGroup,
16
+ DropdownMenuRadioItem,
17
+ DropdownMenuSeparator,
18
+ DropdownMenuTrigger,
19
+ } from "@/components/ui/dropdown-menu"
20
+ import { ConnectionStatus } from "@/components/connection-status"
21
+ import { cn } from "@/lib/utils"
22
+ import type { DatoolClientStream } from "../../shared/types"
23
+
24
+ type StreamControlsProps = {
25
+ streams: DatoolClientStream[]
26
+ selectedStreamId: string | null
27
+ isConnected: boolean
28
+ isConnecting: boolean
29
+ isDisabled?: boolean
30
+ canClear?: boolean
31
+ onClear: () => void
32
+ onPause: () => void
33
+ onPlay: () => void
34
+ onSelectStream: (streamId: string) => void
35
+ className?: string
36
+ }
37
+
38
+ export function StreamControls({
39
+ streams,
40
+ selectedStreamId,
41
+ isConnected,
42
+ isConnecting,
43
+ isDisabled = false,
44
+ canClear = true,
45
+ onClear,
46
+ onPause,
47
+ onPlay,
48
+ onSelectStream,
49
+ className,
50
+ }: StreamControlsProps) {
51
+ const selectedStreamLabel = React.useMemo(() => {
52
+ if (!selectedStreamId) {
53
+ return streams[0]?.label ?? "Select a stream"
54
+ }
55
+
56
+ return (
57
+ streams.find((stream) => stream.id === selectedStreamId)?.label ??
58
+ "Select a stream"
59
+ )
60
+ }, [selectedStreamId, streams])
61
+
62
+ const canOpenMenu = !isDisabled && streams.length > 0
63
+ const canPlay = !isDisabled && Boolean(selectedStreamId) && !isConnecting
64
+ const playButtonLabel = isConnected ? "Pause stream" : "Play stream"
65
+
66
+ return (
67
+ <div className="flex items-center gap-2">
68
+ <div
69
+ className={cn(
70
+ "flex min-w-0 h-10 overflow-hidden rounded-md border border-input bg-background",
71
+ className
72
+ )}
73
+ >
74
+ <DropdownMenu>
75
+ <DropdownMenuTrigger asChild>
76
+ <Button
77
+ type="button"
78
+ variant="ghost"
79
+ className="h-10 min-w-0 border-0 flex-1 justify-between"
80
+ disabled={!canOpenMenu}
81
+ >
82
+ <span className="truncate text-sm">{selectedStreamLabel}</span>
83
+ <ChevronDownIcon className="size-4 text-muted-foreground" />
84
+ </Button>
85
+ </DropdownMenuTrigger>
86
+ <DropdownMenuContent
87
+ align="start"
88
+ sideOffset={10}
89
+ className="max-h-80 max-w-[min(24rem,calc(100vw-2rem))]"
90
+ >
91
+ <div className="px-2 py-1.5">
92
+ <ConnectionStatus
93
+ isConnected={isConnected}
94
+ isConnecting={isConnecting}
95
+ />
96
+ </div>
97
+ <DropdownMenuSeparator />
98
+ <DropdownMenuLabel>Streams</DropdownMenuLabel>
99
+ <DropdownMenuSeparator />
100
+ <DropdownMenuRadioGroup
101
+ value={selectedStreamId ?? ""}
102
+ onValueChange={onSelectStream}
103
+ >
104
+ {streams.map((stream) => (
105
+ <DropdownMenuRadioItem
106
+ key={stream.id}
107
+ value={stream.id}
108
+ className="min-h-9 text-sm"
109
+ >
110
+ {stream.label}
111
+ </DropdownMenuRadioItem>
112
+ ))}
113
+ </DropdownMenuRadioGroup>
114
+ </DropdownMenuContent>
115
+ </DropdownMenu>
116
+ <Button
117
+ type="button"
118
+ variant="ghost"
119
+ size="icon-xl"
120
+ disabled={!canClear}
121
+ aria-label="Clear rows"
122
+ onClick={onClear}
123
+ className="rounded-full"
124
+ >
125
+ <XIcon className="size-5" />
126
+ </Button>
127
+ </div>
128
+
129
+ <Button
130
+ type="button"
131
+ size="xl"
132
+ variant="outline"
133
+ disabled={!canPlay}
134
+ aria-label={playButtonLabel}
135
+ onClick={isConnected ? onPause : onPlay}
136
+ className={
137
+ cn(isConnected ? "border-blue-500 ring-1 ring-blue-500 text-blue-500!" : "",
138
+ "cursor-pointer gap-2"
139
+ )
140
+ }
141
+ >
142
+ <div className={cn("[&_svg]:shrink-0 [&_svg]:size-3",
143
+ isConnecting ? "animate-spin" : ""
144
+ )}>
145
+ {isConnecting ? (
146
+ <LoaderCircleIcon />
147
+ ) : isConnected ? (
148
+ <StopCircleIcon className="[&_rect]:fill-current" />
149
+ ) : (
150
+ <PlayCircleIcon className="[&_path]:fill-current" />
151
+ )}
152
+ </div>
153
+ Live
154
+ </Button>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,230 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import * as React from "react"
3
+
4
+ export type Theme = "dark" | "light" | "system"
5
+ type ResolvedTheme = "dark" | "light"
6
+
7
+ type ThemeProviderProps = {
8
+ children: React.ReactNode
9
+ defaultTheme?: Theme
10
+ storageKey?: string
11
+ disableTransitionOnChange?: boolean
12
+ }
13
+
14
+ type ThemeProviderState = {
15
+ theme: Theme
16
+ setTheme: (theme: Theme) => void
17
+ }
18
+
19
+ const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
20
+ const THEME_VALUES: Theme[] = ["dark", "light", "system"]
21
+
22
+ const ThemeProviderContext = React.createContext<
23
+ ThemeProviderState | undefined
24
+ >(undefined)
25
+
26
+ function isTheme(value: string | null): value is Theme {
27
+ if (value === null) {
28
+ return false
29
+ }
30
+
31
+ return THEME_VALUES.includes(value as Theme)
32
+ }
33
+
34
+ function getSystemTheme(): ResolvedTheme {
35
+ if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
36
+ return "dark"
37
+ }
38
+
39
+ return "light"
40
+ }
41
+
42
+ function disableTransitionsTemporarily() {
43
+ const style = document.createElement("style")
44
+ style.appendChild(
45
+ document.createTextNode(
46
+ "*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
47
+ )
48
+ )
49
+ document.head.appendChild(style)
50
+
51
+ return () => {
52
+ window.getComputedStyle(document.body)
53
+ requestAnimationFrame(() => {
54
+ requestAnimationFrame(() => {
55
+ style.remove()
56
+ })
57
+ })
58
+ }
59
+ }
60
+
61
+ function isEditableTarget(target: EventTarget | null) {
62
+ if (!(target instanceof HTMLElement)) {
63
+ return false
64
+ }
65
+
66
+ if (target.isContentEditable) {
67
+ return true
68
+ }
69
+
70
+ const editableParent = target.closest(
71
+ "input, textarea, select, [contenteditable='true']"
72
+ )
73
+ if (editableParent) {
74
+ return true
75
+ }
76
+
77
+ return false
78
+ }
79
+
80
+ export function ThemeProvider({
81
+ children,
82
+ defaultTheme = "system",
83
+ storageKey = "theme",
84
+ disableTransitionOnChange = true,
85
+ ...props
86
+ }: ThemeProviderProps) {
87
+ const [theme, setThemeState] = React.useState<Theme>(() => {
88
+ const storedTheme = localStorage.getItem(storageKey)
89
+ if (isTheme(storedTheme)) {
90
+ return storedTheme
91
+ }
92
+
93
+ return defaultTheme
94
+ })
95
+
96
+ const setTheme = React.useCallback(
97
+ (nextTheme: Theme) => {
98
+ localStorage.setItem(storageKey, nextTheme)
99
+ setThemeState(nextTheme)
100
+ },
101
+ [storageKey]
102
+ )
103
+
104
+ const applyTheme = React.useCallback(
105
+ (nextTheme: Theme) => {
106
+ const root = document.documentElement
107
+ const resolvedTheme =
108
+ nextTheme === "system" ? getSystemTheme() : nextTheme
109
+ const restoreTransitions = disableTransitionOnChange
110
+ ? disableTransitionsTemporarily()
111
+ : null
112
+
113
+ root.classList.remove("light", "dark")
114
+ root.classList.add(resolvedTheme)
115
+
116
+ if (restoreTransitions) {
117
+ restoreTransitions()
118
+ }
119
+ },
120
+ [disableTransitionOnChange]
121
+ )
122
+
123
+ React.useEffect(() => {
124
+ applyTheme(theme)
125
+
126
+ if (theme !== "system") {
127
+ return undefined
128
+ }
129
+
130
+ const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
131
+ const handleChange = () => {
132
+ applyTheme("system")
133
+ }
134
+
135
+ mediaQuery.addEventListener("change", handleChange)
136
+
137
+ return () => {
138
+ mediaQuery.removeEventListener("change", handleChange)
139
+ }
140
+ }, [theme, applyTheme])
141
+
142
+ React.useEffect(() => {
143
+ const handleKeyDown = (event: KeyboardEvent) => {
144
+ if (event.repeat) {
145
+ return
146
+ }
147
+
148
+ if (event.metaKey || event.ctrlKey || event.altKey) {
149
+ return
150
+ }
151
+
152
+ if (isEditableTarget(event.target)) {
153
+ return
154
+ }
155
+
156
+ if (event.key.toLowerCase() !== "d") {
157
+ return
158
+ }
159
+
160
+ setThemeState((currentTheme) => {
161
+ const nextTheme =
162
+ currentTheme === "dark"
163
+ ? "light"
164
+ : currentTheme === "light"
165
+ ? "dark"
166
+ : getSystemTheme() === "dark"
167
+ ? "light"
168
+ : "dark"
169
+
170
+ localStorage.setItem(storageKey, nextTheme)
171
+ return nextTheme
172
+ })
173
+ }
174
+
175
+ window.addEventListener("keydown", handleKeyDown)
176
+
177
+ return () => {
178
+ window.removeEventListener("keydown", handleKeyDown)
179
+ }
180
+ }, [storageKey])
181
+
182
+ React.useEffect(() => {
183
+ const handleStorageChange = (event: StorageEvent) => {
184
+ if (event.storageArea !== localStorage) {
185
+ return
186
+ }
187
+
188
+ if (event.key !== storageKey) {
189
+ return
190
+ }
191
+
192
+ if (isTheme(event.newValue)) {
193
+ setThemeState(event.newValue)
194
+ return
195
+ }
196
+
197
+ setThemeState(defaultTheme)
198
+ }
199
+
200
+ window.addEventListener("storage", handleStorageChange)
201
+
202
+ return () => {
203
+ window.removeEventListener("storage", handleStorageChange)
204
+ }
205
+ }, [defaultTheme, storageKey])
206
+
207
+ const value = React.useMemo(
208
+ () => ({
209
+ theme,
210
+ setTheme,
211
+ }),
212
+ [theme, setTheme]
213
+ )
214
+
215
+ return (
216
+ <ThemeProviderContext.Provider {...props} value={value}>
217
+ {children}
218
+ </ThemeProviderContext.Provider>
219
+ )
220
+ }
221
+
222
+ export const useTheme = () => {
223
+ const context = React.useContext(ThemeProviderContext)
224
+
225
+ if (context === undefined) {
226
+ throw new Error("useTheme must be used within a ThemeProvider")
227
+ }
228
+
229
+ return context
230
+ }
@@ -0,0 +1,68 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ import * as React from "react"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { Slot } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const buttonVariants = cva(
9
+ "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-gray-900 text-white hover:bg-gray-800",
14
+ outline:
15
+ "border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
+ ghost:
19
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
20
+ destructive:
21
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default:
26
+ "h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
27
+ xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
28
+ sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
29
+ lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
30
+ xl: "h-10 gap-1 px-3 text-sm/relaxed has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-5",
31
+ icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
32
+ "icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
33
+ "icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
34
+ "icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
35
+ "icon-xl": "size-10 [&_svg:not([class*='size-'])]:size-5",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ }
43
+ )
44
+
45
+ function Button({
46
+ className,
47
+ variant = "default",
48
+ size = "default",
49
+ asChild = false,
50
+ ...props
51
+ }: React.ComponentProps<"button"> &
52
+ VariantProps<typeof buttonVariants> & {
53
+ asChild?: boolean
54
+ }) {
55
+ const Comp = asChild ? Slot.Root : "button"
56
+
57
+ return (
58
+ <Comp
59
+ data-slot="button"
60
+ data-variant={variant}
61
+ data-size={size}
62
+ className={cn(buttonVariants({ variant, size, className }))}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ export { Button, buttonVariants }