@silvery/ui 0.3.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/package.json +71 -0
- package/src/animation/easing.ts +38 -0
- package/src/animation/index.ts +18 -0
- package/src/animation/useAnimation.ts +143 -0
- package/src/animation/useInterval.ts +39 -0
- package/src/animation/useLatest.ts +35 -0
- package/src/animation/useTimeout.ts +65 -0
- package/src/animation/useTransition.ts +110 -0
- package/src/animation.ts +24 -0
- package/src/ansi/index.ts +43 -0
- package/src/canvas/index.ts +169 -0
- package/src/cli/ansi.ts +85 -0
- package/src/cli/index.ts +39 -0
- package/src/cli/multi-progress.ts +340 -0
- package/src/cli/progress-bar.ts +222 -0
- package/src/cli/spinner.ts +275 -0
- package/src/components/Badge.tsx +54 -0
- package/src/components/Breadcrumb.tsx +72 -0
- package/src/components/Button.tsx +73 -0
- package/src/components/CommandPalette.tsx +186 -0
- package/src/components/Console.tsx +79 -0
- package/src/components/CursorLine.tsx +71 -0
- package/src/components/Divider.tsx +67 -0
- package/src/components/EditContextDisplay.tsx +164 -0
- package/src/components/ErrorBoundary.tsx +179 -0
- package/src/components/Form.tsx +86 -0
- package/src/components/GridCell.tsx +42 -0
- package/src/components/HorizontalVirtualList.tsx +375 -0
- package/src/components/ModalDialog.tsx +179 -0
- package/src/components/PickerDialog.tsx +208 -0
- package/src/components/PickerList.tsx +93 -0
- package/src/components/ProgressBar.tsx +126 -0
- package/src/components/Screen.tsx +78 -0
- package/src/components/ScrollbackList.tsx +92 -0
- package/src/components/ScrollbackView.tsx +390 -0
- package/src/components/SelectList.tsx +176 -0
- package/src/components/Skeleton.tsx +87 -0
- package/src/components/Spinner.tsx +64 -0
- package/src/components/SplitView.tsx +199 -0
- package/src/components/Table.tsx +139 -0
- package/src/components/Tabs.tsx +203 -0
- package/src/components/TextArea.tsx +264 -0
- package/src/components/TextInput.tsx +240 -0
- package/src/components/Toast.tsx +216 -0
- package/src/components/Toggle.tsx +73 -0
- package/src/components/Tooltip.tsx +60 -0
- package/src/components/TreeView.tsx +212 -0
- package/src/components/Typography.tsx +233 -0
- package/src/components/VirtualList.tsx +318 -0
- package/src/components/VirtualView.tsx +221 -0
- package/src/components/useReadline.ts +213 -0
- package/src/components/useTextArea.ts +648 -0
- package/src/components.ts +133 -0
- package/src/display/Table.tsx +179 -0
- package/src/display/index.ts +13 -0
- package/src/hooks/useTea.ts +133 -0
- package/src/image/Image.tsx +187 -0
- package/src/image/index.ts +15 -0
- package/src/image/kitty-graphics.ts +161 -0
- package/src/image/sixel-encoder.ts +194 -0
- package/src/images.ts +22 -0
- package/src/index.ts +34 -0
- package/src/input/Select.tsx +155 -0
- package/src/input/TextInput.tsx +227 -0
- package/src/input/index.ts +25 -0
- package/src/progress/als-context.ts +160 -0
- package/src/progress/declarative.ts +519 -0
- package/src/progress/index.ts +54 -0
- package/src/progress/step-node.ts +152 -0
- package/src/progress/steps.ts +425 -0
- package/src/progress/task.ts +138 -0
- package/src/progress/tasks.ts +216 -0
- package/src/react/ProgressBar.tsx +146 -0
- package/src/react/Spinner.tsx +74 -0
- package/src/react/Tasks.tsx +144 -0
- package/src/react/context.tsx +145 -0
- package/src/react/index.ts +30 -0
- package/src/types.ts +252 -0
- package/src/utils/eta.ts +155 -0
- package/src/utils/index.ts +13 -0
- package/src/wrappers/index.ts +36 -0
- package/src/wrappers/with-progress.ts +250 -0
- package/src/wrappers/with-select.ts +194 -0
- package/src/wrappers/with-spinner.ts +108 -0
- package/src/wrappers/with-text-input.ts +388 -0
- package/src/wrappers/wrap-emitter.ts +158 -0
- package/src/wrappers/wrap-generator.ts +143 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React context for progress state management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useContext, useState, useCallback } from "react"
|
|
6
|
+
import type { SpinnerStyle } from "../types.js"
|
|
7
|
+
import { Spinner } from "./Spinner"
|
|
8
|
+
|
|
9
|
+
/** Progress context state */
|
|
10
|
+
interface ProgressContextState {
|
|
11
|
+
/** Currently showing a spinner */
|
|
12
|
+
isLoading: boolean
|
|
13
|
+
/** Loading message */
|
|
14
|
+
loadingText: string
|
|
15
|
+
/** Spinner style */
|
|
16
|
+
spinnerStyle: SpinnerStyle
|
|
17
|
+
|
|
18
|
+
/** Show a spinner with message */
|
|
19
|
+
showSpinner: (text: string, style?: SpinnerStyle) => void
|
|
20
|
+
/** Hide the spinner */
|
|
21
|
+
hideSpinner: () => void
|
|
22
|
+
|
|
23
|
+
/** Progress bar state */
|
|
24
|
+
progress: { current: number; total: number } | null
|
|
25
|
+
/** Update progress */
|
|
26
|
+
updateProgress: (current: number, total?: number) => void
|
|
27
|
+
/** Clear progress */
|
|
28
|
+
clearProgress: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ProgressContext = createContext<ProgressContextState | null>(null)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Progress context provider
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* function App() {
|
|
39
|
+
* return (
|
|
40
|
+
* <ProgressProvider>
|
|
41
|
+
* <MyApp />
|
|
42
|
+
* </ProgressProvider>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* function DeepComponent() {
|
|
47
|
+
* const { showSpinner, hideSpinner } = useProgress();
|
|
48
|
+
*
|
|
49
|
+
* const handleLoad = async () => {
|
|
50
|
+
* showSpinner("Loading...");
|
|
51
|
+
* await loadData();
|
|
52
|
+
* hideSpinner();
|
|
53
|
+
* };
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function ProgressProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
58
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
59
|
+
const [loadingText, setLoadingText] = useState("")
|
|
60
|
+
const [spinnerStyle, setSpinnerStyle] = useState<SpinnerStyle>("dots")
|
|
61
|
+
const [progress, setProgress] = useState<{
|
|
62
|
+
current: number
|
|
63
|
+
total: number
|
|
64
|
+
} | null>(null)
|
|
65
|
+
|
|
66
|
+
const showSpinner = useCallback((text: string, style: SpinnerStyle = "dots") => {
|
|
67
|
+
setLoadingText(text)
|
|
68
|
+
setSpinnerStyle(style)
|
|
69
|
+
setIsLoading(true)
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const hideSpinner = useCallback(() => {
|
|
73
|
+
setIsLoading(false)
|
|
74
|
+
setLoadingText("")
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const updateProgress = useCallback((current: number, total?: number) => {
|
|
78
|
+
setProgress((prev) => ({
|
|
79
|
+
current,
|
|
80
|
+
total: total ?? prev?.total ?? 100,
|
|
81
|
+
}))
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
const clearProgress = useCallback(() => {
|
|
85
|
+
setProgress(null)
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
const value: ProgressContextState = {
|
|
89
|
+
isLoading,
|
|
90
|
+
loadingText,
|
|
91
|
+
spinnerStyle,
|
|
92
|
+
showSpinner,
|
|
93
|
+
hideSpinner,
|
|
94
|
+
progress,
|
|
95
|
+
updateProgress,
|
|
96
|
+
clearProgress,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <ProgressContext.Provider value={value}>{children}</ProgressContext.Provider>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Hook to access progress context
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* function LoadingOverlay() {
|
|
108
|
+
* const { isLoading, loadingText, spinnerStyle } = useProgress();
|
|
109
|
+
*
|
|
110
|
+
* if (!isLoading) return null;
|
|
111
|
+
*
|
|
112
|
+
* return <Spinner label={loadingText} style={spinnerStyle} />;
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function useProgress(): ProgressContextState {
|
|
117
|
+
const context = useContext(ProgressContext)
|
|
118
|
+
|
|
119
|
+
if (!context) {
|
|
120
|
+
throw new Error("useProgress must be used within a ProgressProvider")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return context
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Component that renders spinner when loading
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <ProgressProvider>
|
|
132
|
+
* <ProgressIndicator />
|
|
133
|
+
* <MainContent />
|
|
134
|
+
* </ProgressProvider>
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function ProgressIndicator(): React.ReactElement | null {
|
|
138
|
+
const { isLoading, loadingText, spinnerStyle } = useProgress()
|
|
139
|
+
|
|
140
|
+
if (!isLoading) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return <Spinner label={loadingText} style={spinnerStyle} />
|
|
145
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React components for TUI progress indicators
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { Spinner, ProgressBar, Tasks, Task, useProgress } from "@silvery/ui/react";
|
|
7
|
+
*
|
|
8
|
+
* // Spinner
|
|
9
|
+
* <Spinner label="Loading..." style="dots" />
|
|
10
|
+
*
|
|
11
|
+
* // Progress bar
|
|
12
|
+
* <ProgressBar value={50} total={100} showPercentage showETA />
|
|
13
|
+
*
|
|
14
|
+
* // Task list
|
|
15
|
+
* <Tasks>
|
|
16
|
+
* <Task title="Scanning" status="completed" />
|
|
17
|
+
* <Task title="Processing" status="running" />
|
|
18
|
+
* </Tasks>
|
|
19
|
+
*
|
|
20
|
+
* // Context for nested components
|
|
21
|
+
* <ProgressProvider>
|
|
22
|
+
* <App />
|
|
23
|
+
* </ProgressProvider>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { Spinner, useSpinnerFrame } from "./Spinner"
|
|
28
|
+
export { ProgressBar, useProgressBar } from "./ProgressBar"
|
|
29
|
+
export { Task, Tasks, useTasks } from "./Tasks"
|
|
30
|
+
export { ProgressProvider, useProgress, ProgressIndicator } from "./context"
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for silvery-ui progress components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Progress info passed to callbacks (legacy, use StepProgress for steps()) */
|
|
6
|
+
export interface ProgressInfo {
|
|
7
|
+
phase?: string
|
|
8
|
+
current: number
|
|
9
|
+
total: number
|
|
10
|
+
detail?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Progress info yielded from step generators
|
|
15
|
+
*
|
|
16
|
+
* Yield with `label` to create/update a sub-step:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* function* loadRepo() {
|
|
19
|
+
* yield { label: "Discovering files" };
|
|
20
|
+
* // ... do work ...
|
|
21
|
+
* yield { label: "Parsing markdown", current: 0, total: 100 };
|
|
22
|
+
* // ... do work ...
|
|
23
|
+
* yield { label: "Parsing markdown", current: 100, total: 100 };
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface StepProgress {
|
|
28
|
+
/** Display label for sub-step (changing label creates new sub-step) */
|
|
29
|
+
label?: string
|
|
30
|
+
/** Current progress count */
|
|
31
|
+
current?: number
|
|
32
|
+
/** Total count for progress display */
|
|
33
|
+
total?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Callback signature for progress reporting */
|
|
37
|
+
export type ProgressCallback = (info: ProgressInfo) => void
|
|
38
|
+
|
|
39
|
+
/** Generator that yields progress info */
|
|
40
|
+
export type ProgressGenerator<T = void> = Generator<{ current: number; total: number }, T, unknown>
|
|
41
|
+
|
|
42
|
+
/** Spinner animation styles */
|
|
43
|
+
export type SpinnerStyle = "dots" | "line" | "arc" | "bounce" | "pulse"
|
|
44
|
+
|
|
45
|
+
/** Task status for multi-task display */
|
|
46
|
+
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "skipped"
|
|
47
|
+
|
|
48
|
+
/** Options for Spinner class */
|
|
49
|
+
export interface SpinnerOptions {
|
|
50
|
+
/** Initial text to display */
|
|
51
|
+
text?: string
|
|
52
|
+
/** Animation style */
|
|
53
|
+
style?: SpinnerStyle
|
|
54
|
+
/** Spinner color (chalk color name) */
|
|
55
|
+
color?: string
|
|
56
|
+
/** Output stream (default: process.stdout) */
|
|
57
|
+
stream?: NodeJS.WriteStream
|
|
58
|
+
/** Hide cursor during spinner (default: true) */
|
|
59
|
+
hideCursor?: boolean
|
|
60
|
+
/** Animation interval in ms (default: 80) */
|
|
61
|
+
interval?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Options for ProgressBar class */
|
|
65
|
+
export interface ProgressBarOptions {
|
|
66
|
+
/** Total value for progress calculation */
|
|
67
|
+
total?: number
|
|
68
|
+
/** Format string with placeholders: :bar :percent :current :total :eta :rate :phase */
|
|
69
|
+
format?: string
|
|
70
|
+
/** Width of the progress bar in characters (default: 40) */
|
|
71
|
+
width?: number
|
|
72
|
+
/** Character for completed portion (default: "█") */
|
|
73
|
+
complete?: string
|
|
74
|
+
/** Character for incomplete portion (default: "░") */
|
|
75
|
+
incomplete?: string
|
|
76
|
+
/** Show percentage (default: true) */
|
|
77
|
+
showPercentage?: boolean
|
|
78
|
+
/** Show ETA (default: true) */
|
|
79
|
+
showETA?: boolean
|
|
80
|
+
/** Output stream (default: process.stdout) */
|
|
81
|
+
stream?: NodeJS.WriteStream
|
|
82
|
+
/** Hide cursor during progress (default: true) */
|
|
83
|
+
hideCursor?: boolean
|
|
84
|
+
/** Phase names for multi-phase progress */
|
|
85
|
+
phases?: Record<string, string>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Options for withSpinner wrapper */
|
|
89
|
+
export interface WithSpinnerOptions {
|
|
90
|
+
/** Spinner style */
|
|
91
|
+
style?: SpinnerStyle
|
|
92
|
+
/** Clear the spinner output on completion */
|
|
93
|
+
clearOnComplete?: boolean
|
|
94
|
+
/** Color for the spinner */
|
|
95
|
+
color?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Options for withProgress wrapper */
|
|
99
|
+
export interface WithProgressOptions {
|
|
100
|
+
/** Map of phase keys to display names */
|
|
101
|
+
phases?: Record<string, string>
|
|
102
|
+
/** Format string for progress bar */
|
|
103
|
+
format?: string
|
|
104
|
+
/** Clear output on completion */
|
|
105
|
+
clearOnComplete?: boolean
|
|
106
|
+
/** Show initial loading message after this many ms (default: 1000). Set to 0 to show immediately. */
|
|
107
|
+
showAfter?: number
|
|
108
|
+
/** Initial loading message to show before progress starts (default: "Loading...") */
|
|
109
|
+
initialMessage?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Task state for Tasks component */
|
|
113
|
+
export interface TaskState {
|
|
114
|
+
id: string
|
|
115
|
+
title: string
|
|
116
|
+
status: TaskStatus
|
|
117
|
+
progress?: { current: number; total: number }
|
|
118
|
+
error?: Error
|
|
119
|
+
children?: TaskState[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Props for React Spinner component */
|
|
123
|
+
export interface SpinnerProps {
|
|
124
|
+
/** Label text to display */
|
|
125
|
+
label?: string
|
|
126
|
+
/** Animation style */
|
|
127
|
+
style?: SpinnerStyle
|
|
128
|
+
/** Spinner color */
|
|
129
|
+
color?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Props for React ProgressBar component */
|
|
133
|
+
export interface ProgressBarProps {
|
|
134
|
+
/** Current value */
|
|
135
|
+
value: number
|
|
136
|
+
/** Total value */
|
|
137
|
+
total: number
|
|
138
|
+
/** Width in characters */
|
|
139
|
+
width?: number
|
|
140
|
+
/** Show percentage */
|
|
141
|
+
showPercentage?: boolean
|
|
142
|
+
/** Show ETA */
|
|
143
|
+
showETA?: boolean
|
|
144
|
+
/** Label text */
|
|
145
|
+
label?: string
|
|
146
|
+
/** Color for completed portion */
|
|
147
|
+
color?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Props for React Task component */
|
|
151
|
+
export interface TaskProps {
|
|
152
|
+
/** Task title */
|
|
153
|
+
title: string
|
|
154
|
+
/** Task status */
|
|
155
|
+
status: TaskStatus
|
|
156
|
+
/** Children (e.g., nested progress bar) */
|
|
157
|
+
children?: React.ReactNode
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Props for React TextInput component */
|
|
161
|
+
export interface TextInputProps {
|
|
162
|
+
/** Current input value (controlled) */
|
|
163
|
+
value: string
|
|
164
|
+
/** Called when value changes */
|
|
165
|
+
onChange: (value: string) => void
|
|
166
|
+
/** Placeholder text when empty */
|
|
167
|
+
placeholder?: string
|
|
168
|
+
/** Mask character for password input (e.g., "*") */
|
|
169
|
+
mask?: string
|
|
170
|
+
/** Autocomplete suggestions */
|
|
171
|
+
autocomplete?: string[]
|
|
172
|
+
/** Called when autocomplete suggestion is selected */
|
|
173
|
+
onAutocomplete?: (suggestion: string) => void
|
|
174
|
+
/** Called when Enter is pressed */
|
|
175
|
+
onSubmit?: (value: string) => void
|
|
176
|
+
/** Cursor position (for rendering) */
|
|
177
|
+
cursorPosition?: number
|
|
178
|
+
/** Whether input is focused */
|
|
179
|
+
focused?: boolean
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Options for withTextInput CLI wrapper */
|
|
183
|
+
export interface TextInputOptions {
|
|
184
|
+
/** Placeholder text shown when input is empty */
|
|
185
|
+
placeholder?: string
|
|
186
|
+
/** Mask character for password input (e.g., "*") */
|
|
187
|
+
mask?: string
|
|
188
|
+
/** Validation function - return error message or undefined if valid */
|
|
189
|
+
validate?: (value: string) => string | undefined
|
|
190
|
+
/** Autocomplete suggestions */
|
|
191
|
+
autocomplete?: string[]
|
|
192
|
+
/** Default value */
|
|
193
|
+
defaultValue?: string
|
|
194
|
+
/** Output stream (default: process.stdout) */
|
|
195
|
+
stream?: NodeJS.WriteStream
|
|
196
|
+
/** Input stream (default: process.stdin) */
|
|
197
|
+
inputStream?: NodeJS.ReadStream
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Column definition for Table component */
|
|
201
|
+
export interface TableColumn {
|
|
202
|
+
/** Key in data objects to display */
|
|
203
|
+
key: string
|
|
204
|
+
/** Header text */
|
|
205
|
+
header: string
|
|
206
|
+
/** Fixed column width (auto-calculated from content if not specified) */
|
|
207
|
+
width?: number
|
|
208
|
+
/** Text alignment within the column */
|
|
209
|
+
align?: "left" | "center" | "right"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Props for React Table component */
|
|
213
|
+
export interface TableProps {
|
|
214
|
+
/** Column definitions */
|
|
215
|
+
columns: TableColumn[]
|
|
216
|
+
/** Data rows to display */
|
|
217
|
+
data: Array<Record<string, unknown>>
|
|
218
|
+
/** Show box borders around cells */
|
|
219
|
+
border?: boolean
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Option for Select component */
|
|
223
|
+
export interface SelectOption<T> {
|
|
224
|
+
/** Display text for this option */
|
|
225
|
+
label: string
|
|
226
|
+
/** Value returned when selected */
|
|
227
|
+
value: T
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Props for React Select component */
|
|
231
|
+
export interface SelectProps<T> {
|
|
232
|
+
/** Available options */
|
|
233
|
+
options: SelectOption<T>[]
|
|
234
|
+
/** Currently selected value */
|
|
235
|
+
value?: T
|
|
236
|
+
/** Called when selection changes */
|
|
237
|
+
onChange?: (value: T) => void
|
|
238
|
+
/** Maximum number of visible options (default: 10) */
|
|
239
|
+
maxVisible?: number
|
|
240
|
+
/** Controlled highlight index for keyboard navigation */
|
|
241
|
+
highlightIndex?: number
|
|
242
|
+
/** Called when highlight index changes */
|
|
243
|
+
onHighlightChange?: (index: number) => void
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Options for withSelect CLI wrapper */
|
|
247
|
+
export interface WithSelectOptions {
|
|
248
|
+
/** Initial highlighted index (default: 0) */
|
|
249
|
+
initial?: number
|
|
250
|
+
/** Maximum number of visible options (default: 10) */
|
|
251
|
+
maxVisible?: number
|
|
252
|
+
}
|
package/src/utils/eta.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ETA calculation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Sample point for ETA calculation */
|
|
6
|
+
export interface ETASample {
|
|
7
|
+
time: number
|
|
8
|
+
value: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** ETA calculation result */
|
|
12
|
+
export interface ETAResult {
|
|
13
|
+
/** Estimated seconds remaining, or null if insufficient data */
|
|
14
|
+
seconds: number | null
|
|
15
|
+
/** Formatted ETA string (e.g., "1:30", "2:15:30", "--:--") */
|
|
16
|
+
formatted: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate ETA from a buffer of samples
|
|
21
|
+
*
|
|
22
|
+
* @param buffer - Array of {time, value} samples
|
|
23
|
+
* @param current - Current progress value
|
|
24
|
+
* @param total - Total target value
|
|
25
|
+
* @returns ETA in seconds (null if insufficient data)
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const buffer = [
|
|
30
|
+
* { time: 1000, value: 0 },
|
|
31
|
+
* { time: 2000, value: 10 },
|
|
32
|
+
* ];
|
|
33
|
+
* const eta = calculateETA(buffer, 10, 100);
|
|
34
|
+
* // eta = 9 (9 seconds remaining at 10 items/sec)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function calculateETA(buffer: ETASample[], current: number, total: number): number | null {
|
|
38
|
+
if (buffer.length < 2) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const first = buffer[0]!
|
|
43
|
+
const last = buffer[buffer.length - 1]!
|
|
44
|
+
|
|
45
|
+
const elapsed = (last.time - first.time) / 1000 // seconds
|
|
46
|
+
const progress = last.value - first.value
|
|
47
|
+
|
|
48
|
+
if (elapsed <= 0 || progress <= 0) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rate = progress / elapsed // items per second
|
|
53
|
+
const remaining = total - current
|
|
54
|
+
|
|
55
|
+
return remaining / rate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format ETA seconds as human-readable string
|
|
60
|
+
*
|
|
61
|
+
* @param eta - ETA in seconds (null for unknown)
|
|
62
|
+
* @returns Formatted string (e.g., "1:30", "2:15:30", "--:--", ">1d")
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* formatETA(90) // "1:30"
|
|
67
|
+
* formatETA(3665) // "1:01:05"
|
|
68
|
+
* formatETA(null) // "--:--"
|
|
69
|
+
* formatETA(100000) // ">1d"
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function formatETA(eta: number | null): string {
|
|
73
|
+
if (eta === null || !isFinite(eta)) {
|
|
74
|
+
return "--:--"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (eta > 86400) {
|
|
78
|
+
// > 24 hours
|
|
79
|
+
return ">1d"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hours = Math.floor(eta / 3600)
|
|
83
|
+
const minutes = Math.floor((eta % 3600) / 60)
|
|
84
|
+
const seconds = Math.floor(eta % 60)
|
|
85
|
+
|
|
86
|
+
if (hours > 0) {
|
|
87
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Calculate and format ETA in one call
|
|
95
|
+
*
|
|
96
|
+
* @param buffer - Array of {time, value} samples
|
|
97
|
+
* @param current - Current progress value
|
|
98
|
+
* @param total - Total target value
|
|
99
|
+
* @returns Object with seconds (number|null) and formatted string
|
|
100
|
+
*/
|
|
101
|
+
export function getETA(buffer: ETASample[], current: number, total: number): ETAResult {
|
|
102
|
+
const seconds = calculateETA(buffer, current, total)
|
|
103
|
+
return {
|
|
104
|
+
seconds,
|
|
105
|
+
formatted: formatETA(seconds),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Default buffer size for ETA smoothing */
|
|
110
|
+
export const DEFAULT_ETA_BUFFER_SIZE = 10
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create an ETA tracker with automatic buffer management
|
|
114
|
+
*
|
|
115
|
+
* @param bufferSize - Number of samples to keep (default: 10)
|
|
116
|
+
* @returns ETA tracker object
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const tracker = createETATracker();
|
|
121
|
+
* tracker.record(0);
|
|
122
|
+
* // ... later ...
|
|
123
|
+
* tracker.record(50);
|
|
124
|
+
* const eta = tracker.getETA(50, 100);
|
|
125
|
+
* console.log(eta.formatted); // "0:30"
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function createETATracker(bufferSize = DEFAULT_ETA_BUFFER_SIZE) {
|
|
129
|
+
const buffer: ETASample[] = []
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
/** Record a new sample */
|
|
133
|
+
record(value: number): void {
|
|
134
|
+
buffer.push({ time: Date.now(), value })
|
|
135
|
+
if (buffer.length > bufferSize) {
|
|
136
|
+
buffer.shift()
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** Get current ETA */
|
|
141
|
+
getETA(current: number, total: number): ETAResult {
|
|
142
|
+
return getETA(buffer, current, total)
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/** Reset the buffer */
|
|
146
|
+
reset(): void {
|
|
147
|
+
buffer.length = 0
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** Get buffer for external use */
|
|
151
|
+
getBuffer(): readonly ETASample[] {
|
|
152
|
+
return buffer
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress wrappers - Adapt existing async patterns
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import {
|
|
7
|
+
* withSpinner,
|
|
8
|
+
* withProgress,
|
|
9
|
+
* wrapGenerator,
|
|
10
|
+
* wrapEmitter
|
|
11
|
+
* } from "@silvery/ui/wrappers";
|
|
12
|
+
*
|
|
13
|
+
* // Wrap any promise
|
|
14
|
+
* const data = await withSpinner(fetchData(), "Loading...");
|
|
15
|
+
*
|
|
16
|
+
* // Wrap callback-based APIs (like km sync)
|
|
17
|
+
* await withProgress(
|
|
18
|
+
* (onProgress) => manager.syncFromFs(onProgress),
|
|
19
|
+
* { phases: { scanning: "Scanning", reconciling: "Reconciling" } }
|
|
20
|
+
* );
|
|
21
|
+
*
|
|
22
|
+
* // Wrap generators
|
|
23
|
+
* await wrapGenerator(evaluateAllRules(), "Evaluating rules");
|
|
24
|
+
*
|
|
25
|
+
* // Track EventEmitter state
|
|
26
|
+
* wrapEmitter(manager, { events: { ready: { succeed: true } } });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export { withSpinner, attachSpinner } from "./with-spinner"
|
|
31
|
+
export { withProgress, createProgressCallback } from "./with-progress"
|
|
32
|
+
export { wrapGenerator, withIterableProgress } from "./wrap-generator"
|
|
33
|
+
export { wrapEmitter, waitForEvent } from "./wrap-emitter"
|
|
34
|
+
export { withSelect, createSelect } from "./with-select"
|
|
35
|
+
export { withTextInput, createTextInput } from "./with-text-input"
|
|
36
|
+
export type { ProgressCallback, ProgressInfo, TextInputOptions } from "../types.js"
|