@reliverse/rempts-tui 2.3.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 ADDED
@@ -0,0 +1,390 @@
1
+ # rempts-tui
2
+
3
+ A React-based Terminal User Interface library for Rempts CLI framework, powered by OpenTUI's React renderer.
4
+
5
+ ## Features
6
+
7
+ - **React-based Components**: Build TUIs using familiar React patterns and JSX
8
+ - **Component Library**: Pre-built form components like `Form`, `FormField`, `SelectField`, and `ProgressBar`
9
+ - **OpenTUI Integration**: Full access to OpenTUI's React hooks and components
10
+ - **Type Safety**: Complete TypeScript support with proper type inference
11
+ - **Animation Support**: Built-in timeline system for smooth animations
12
+ - **Keyboard Handling**: Easy keyboard event management with `useKeyboard`
13
+ - **First-Class TUI Support**: TUI rendering is a first-class feature, not a plugin
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add rempts-tui react
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { createCLI, defineCommand } from '@reliverse/rempts-core'
25
+ import { registerTuiRenderer } from '@reliverse/rempts-tui'
26
+
27
+ const cli = await createCLI({
28
+ name: 'my-app',
29
+ version: '1.0.0'
30
+ })
31
+
32
+ // Register TUI renderer to enable render() functions
33
+ registerTuiRenderer()
34
+
35
+ // Define a command with TUI using the render property
36
+ const myCommand = defineCommand({
37
+ name: 'deploy',
38
+ description: 'Deploy application',
39
+ render: () => (
40
+ <box title="Deployment" style={{ border: true, padding: 2 }}>
41
+ <text>Deploying...</text>
42
+ </box>
43
+ ),
44
+ handler: async () => {
45
+ // CLI mode handler (used with --no-tui or in non-interactive environments)
46
+ console.log('Deploying application...')
47
+ }
48
+ })
49
+
50
+ cli.command(myCommand)
51
+ await cli.run()
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Basic TUI Component
57
+
58
+ ```typescript
59
+ import { defineCommand } from '@reliverse/rempts-core'
60
+
61
+ function MyTUI() {
62
+ return (
63
+ <box title="My App" style={{ border: true, padding: 2 }}>
64
+ <text>Hello from My App!</text>
65
+ </box>
66
+ )
67
+ }
68
+
69
+ export const myCommand = defineCommand({
70
+ name: 'my-command',
71
+ description: 'My command with TUI',
72
+ render: () => <MyTUI />,
73
+ handler: async () => {
74
+ console.log('Running my-command in CLI mode')
75
+ }
76
+ })
77
+ ```
78
+
79
+ ### Using Form Components
80
+
81
+ ```typescript
82
+ import { defineCommand } from '@reliverse/rempts-core'
83
+ import { FormField, SelectField } from '@reliverse/rempts-tui'
84
+ import type { SelectOption } from '@opentui/core'
85
+ import { useState } from 'react'
86
+
87
+ function ConfigTUI() {
88
+ const [apiUrl, setApiUrl] = useState('')
89
+ const [region, setRegion] = useState('us-east')
90
+
91
+ const regions: SelectOption[] = [
92
+ { name: 'US East', value: 'us-east', description: 'US East region' },
93
+ { name: 'US West', value: 'us-west', description: 'US West region' }
94
+ ]
95
+
96
+ return (
97
+ <box title="Configure Settings" border padding={2} style={{ flexDirection: 'column' }}>
98
+ <FormField
99
+ label="API URL"
100
+ name="apiUrl"
101
+ placeholder="https://api.example.com"
102
+ required
103
+ value={apiUrl}
104
+ onChange={setApiUrl}
105
+ />
106
+ <SelectField
107
+ label="Region"
108
+ name="region"
109
+ options={regions}
110
+ onChange={setRegion}
111
+ />
112
+ </box>
113
+ )
114
+ }
115
+
116
+ export const configureCommand = defineCommand({
117
+ name: 'configure',
118
+ description: 'Configure application settings',
119
+ render: () => <ConfigTUI />
120
+ })
121
+ ```
122
+
123
+ ### Using OpenTUI Hooks
124
+
125
+ ```typescript
126
+ import { useKeyboard, useTimeline, useTerminalDimensions } from '@reliverse/rempts-tui'
127
+
128
+ function InteractiveTUI({ command }) {
129
+ const [count, setCount] = useState(0)
130
+ const { width, height } = useTerminalDimensions()
131
+
132
+ const timeline = useTimeline({ duration: 2000 })
133
+
134
+ useKeyboard((key) => {
135
+ if (key.name === 'q') {
136
+ process.exit(0)
137
+ }
138
+ if (key.name === 'space') {
139
+ setCount(prev => prev + 1)
140
+ }
141
+ })
142
+
143
+ useEffect(() => {
144
+ timeline.add({ count: 0 }, {
145
+ count: 100,
146
+ duration: 2000,
147
+ onUpdate: (anim) => setCount(anim.targets[0].count)
148
+ })
149
+ }, [])
150
+
151
+ return (
152
+ <box title="Interactive Demo" style={{ border: true, padding: 2 }}>
153
+ <text>Count: {count}</text>
154
+ <text>Terminal: {width}x{height}</text>
155
+ <text>Press SPACE to increment, Q to quit</text>
156
+ </box>
157
+ )
158
+ }
159
+ ```
160
+
161
+ ## Component Library
162
+
163
+ ### Form
164
+
165
+ A container component for building forms with keyboard navigation.
166
+
167
+ ```typescript
168
+ <Form
169
+ title="My Form"
170
+ onSubmit={(values) => console.log(values)}
171
+ onCancel={() => process.exit(0)}
172
+ >
173
+ {/* Form fields */}
174
+ </Form>
175
+ ```
176
+
177
+ **Props:**
178
+
179
+ - `title: string` - Form title
180
+ - `onSubmit: (values: Record<string, any>) => void` - Submit handler
181
+ - `onCancel?: () => void` - Cancel handler (optional)
182
+
183
+ ### FormField
184
+
185
+ A text input field with label and validation.
186
+
187
+ ```typescript
188
+ <FormField
189
+ label="Username"
190
+ name="username"
191
+ placeholder="Enter username"
192
+ required
193
+ value={username}
194
+ onChange={setUsername}
195
+ onSubmit={handleSubmit}
196
+ />
197
+ ```
198
+
199
+ **Props:**
200
+
201
+ - `label: string` - Field label
202
+ - `name: string` - Field name
203
+ - `placeholder?: string` - Placeholder text
204
+ - `required?: boolean` - Whether field is required
205
+ - `value?: string` - Current value
206
+ - `onChange?: (value: string) => void` - Change handler
207
+ - `onSubmit?: (value: string) => void` - Submit handler
208
+
209
+ ### SelectField
210
+
211
+ A dropdown selection field.
212
+
213
+ ```typescript
214
+ <SelectField
215
+ label="Environment"
216
+ name="env"
217
+ options={[
218
+ { name: 'Development', value: 'dev', description: 'Development environment' },
219
+ { name: 'Production', value: 'prod', description: 'Production environment' }
220
+ ]}
221
+ onChange={setEnvironment}
222
+ />
223
+ ```
224
+
225
+ **Props:**
226
+
227
+ - `label: string` - Field label
228
+ - `name: string` - Field name
229
+ - `options: SelectOption[]` - Available options
230
+ - `required?: boolean` - Whether field is required
231
+ - `onChange?: (value: string) => void` - Change handler
232
+
233
+ ### ProgressBar
234
+
235
+ A progress bar component for showing completion status.
236
+
237
+ ```typescript
238
+ <ProgressBar
239
+ value={75}
240
+ label="Upload Progress"
241
+ color="#00ff00"
242
+ />
243
+ ```
244
+
245
+ **Props:**
246
+
247
+ - `value: number` - Progress value (0-100)
248
+ - `label?: string` - Progress label
249
+ - `color?: string` - Progress bar color
250
+
251
+ ## OpenTUI Hooks
252
+
253
+ The plugin re-exports useful OpenTUI React hooks:
254
+
255
+ ### useKeyboard
256
+
257
+ Handle keyboard events.
258
+
259
+ ```typescript
260
+ import { useKeyboard } from '@reliverse/rempts-tui'
261
+
262
+ useKeyboard((key) => {
263
+ if (key.name === 'escape') {
264
+ process.exit(0)
265
+ }
266
+ })
267
+ ```
268
+
269
+ ### useRenderer
270
+
271
+ Access the OpenTUI renderer instance.
272
+
273
+ ```typescript
274
+ import { useRenderer } from '@reliverse/rempts-tui'
275
+
276
+ const renderer = useRenderer()
277
+ renderer.console.show()
278
+ ```
279
+
280
+ ### useTerminalDimensions
281
+
282
+ Get current terminal dimensions.
283
+
284
+ ```typescript
285
+ import { useTerminalDimensions } from '@reliverse/rempts-tui'
286
+
287
+ const { width, height } = useTerminalDimensions()
288
+ ```
289
+
290
+ ### useTimeline
291
+
292
+ Create and manage animations.
293
+
294
+ ```typescript
295
+ import { useTimeline } from '@reliverse/rempts-tui'
296
+
297
+ const timeline = useTimeline({ duration: 2000 })
298
+
299
+ timeline.add({ x: 0 }, {
300
+ x: 100,
301
+ duration: 2000,
302
+ onUpdate: (anim) => setX(anim.targets[0].x)
303
+ })
304
+ ```
305
+
306
+ ### useOnResize
307
+
308
+ Handle terminal resize events.
309
+
310
+ ```typescript
311
+ import { useOnResize } from '@reliverse/rempts-tui'
312
+
313
+ useOnResize((width, height) => {
314
+ console.log(`Terminal resized to ${width}x${height}`)
315
+ })
316
+ ```
317
+
318
+ ## Plugin Configuration
319
+
320
+ ```typescript
321
+ import { tuiPlugin } from '@reliverse/rempts-tui'
322
+
323
+ const plugin = tuiPlugin({
324
+ renderer: {
325
+ exitOnCtrlC: false,
326
+ targetFps: 30,
327
+ enableMouseMovement: true
328
+ },
329
+ theme: 'dark',
330
+ autoForm: false
331
+ })
332
+ ```
333
+
334
+ **Options:**
335
+
336
+ - `renderer?: CliRendererConfig` - OpenTUI renderer configuration
337
+ - `theme?: 'light' | 'dark' | ThemeConfig` - Theme configuration
338
+ - `autoForm?: boolean` - Enable auto-form generation (disabled for now)
339
+
340
+ ## OpenTUI Components
341
+
342
+ You can use any OpenTUI React components directly:
343
+
344
+ ```typescript
345
+ import { render } from '@opentui/react'
346
+
347
+ function MyComponent() {
348
+ return (
349
+ <box style={{ border: true, padding: 2 }}>
350
+ <text>Hello World</text>
351
+ <input placeholder="Type here..." />
352
+ <select options={options} />
353
+ </box>
354
+ )
355
+ }
356
+ ```
357
+
358
+ Available components:
359
+
360
+ - `<box>` - Container with borders and layout
361
+ - `<text>` - Text display with styling
362
+ - `<input>` - Text input field
363
+ - `<select>` - Dropdown selection
364
+ - `<scrollbox>` - Scrollable container
365
+ - `<ascii-font>` - ASCII art text
366
+ - `<tab-select>` - Tab-based selection
367
+
368
+ ## Examples
369
+
370
+ See the `examples/tui-demo` directory for complete examples:
371
+
372
+ - **Deploy Command**: Animated progress bar with timeline
373
+ - **Configure Command**: Form with input and select fields
374
+
375
+ ## TypeScript Support
376
+
377
+ The plugin provides full TypeScript support:
378
+
379
+ ```typescript
380
+ import type { TuiComponent, TuiComponentProps } from '@reliverse/rempts-tui'
381
+
382
+ const MyTUI: TuiComponent = ({ command, args, store }) => {
383
+ // Fully typed props
384
+ return <box>{command.name}</box>
385
+ }
386
+ ```
387
+
388
+ ## License
389
+
390
+ MIT
@@ -0,0 +1,7 @@
1
+ export interface FormProps {
2
+ title: string;
3
+ onSubmit: (values: Record<string, any>) => void;
4
+ onCancel?: () => void;
5
+ children: React.ReactNode;
6
+ }
7
+ export declare function Form({ title, onSubmit, onCancel, children }: FormProps): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { useCallback, useState } from "react";
3
+
4
+ export interface FormProps {
5
+ title: string;
6
+ onSubmit: (values: Record<string, any>) => void;
7
+ onCancel?: () => void;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export function Form({ title, onSubmit, onCancel, children }: FormProps) {
12
+ const [formValues, setFormValues] = useState<Record<string, any>>({});
13
+
14
+ const _handleFieldChange = useCallback((name: string, value: any) => {
15
+ setFormValues((prev) => ({ ...prev, [name]: value }));
16
+ }, []);
17
+
18
+ useKeyboard((key) => {
19
+ if (key.name === "escape" && onCancel) {
20
+ onCancel();
21
+ }
22
+ if (key.name === "enter") {
23
+ onSubmit(formValues);
24
+ }
25
+ });
26
+
27
+ return (
28
+ <box border padding={2} style={{ flexDirection: "column" }} title={title}>
29
+ {children}
30
+ <box style={{ flexDirection: "row", gap: 2, marginTop: 2 }}>
31
+ <text content="Press Enter to submit, Esc to cancel" />
32
+ </box>
33
+ </box>
34
+ );
35
+ }
@@ -0,0 +1,10 @@
1
+ export interface FormFieldProps {
2
+ label: string;
3
+ name: string;
4
+ placeholder?: string;
5
+ required?: boolean;
6
+ value?: string;
7
+ onChange?: (value: string) => void;
8
+ onSubmit?: (value: string) => void;
9
+ }
10
+ export declare function FormField({ label, name, placeholder, required, value: initialValue, onChange, onSubmit, }: FormFieldProps): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { useState } from "react";
2
+
3
+ export interface FormFieldProps {
4
+ label: string;
5
+ name: string;
6
+ placeholder?: string;
7
+ required?: boolean;
8
+ value?: string;
9
+ onChange?: (value: string) => void;
10
+ onSubmit?: (value: string) => void;
11
+ }
12
+
13
+ export function FormField({
14
+ label,
15
+ name,
16
+ placeholder,
17
+ required,
18
+ value: initialValue = "",
19
+ onChange,
20
+ onSubmit,
21
+ }: FormFieldProps) {
22
+ const [_value, setValue] = useState(initialValue);
23
+
24
+ const handleInput = (newValue: string) => {
25
+ setValue(newValue);
26
+ onChange?.(newValue);
27
+ };
28
+
29
+ const handleSubmit = (submittedValue: string) => {
30
+ onSubmit?.(submittedValue);
31
+ };
32
+
33
+ return (
34
+ <box style={{ flexDirection: "column", marginBottom: 1 }}>
35
+ <text content={`${label}${required ? " *" : ""}`} />
36
+ <box border height={3} style={{ marginTop: 0.5 }} title={label}>
37
+ <input
38
+ focused={true}
39
+ onInput={handleInput}
40
+ onSubmit={handleSubmit}
41
+ placeholder={placeholder}
42
+ style={{ focusedBackgroundColor: "#000000" }}
43
+ />
44
+ </box>
45
+ </box>
46
+ );
47
+ }
@@ -0,0 +1,6 @@
1
+ export interface ProgressBarProps {
2
+ value: number;
3
+ label?: string;
4
+ color?: string;
5
+ }
6
+ export declare function ProgressBar({ value, label, color }: ProgressBarProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ export interface ProgressBarProps {
2
+ value: number; // 0-100
3
+ label?: string;
4
+ color?: string;
5
+ }
6
+
7
+ export function ProgressBar({ value, label, color = "#00ff00" }: ProgressBarProps) {
8
+ const clampedValue = Math.max(0, Math.min(100, value));
9
+
10
+ return (
11
+ <box style={{ flexDirection: "column" }}>
12
+ {label && <text content={label} />}
13
+ <box style={{ backgroundColor: "#333", height: 3, marginTop: 0.5 }}>
14
+ <box
15
+ style={{
16
+ width: `${clampedValue}%`,
17
+ backgroundColor: color,
18
+ height: 1,
19
+ }}
20
+ />
21
+ </box>
22
+ <text content={`${Math.floor(clampedValue)}%`} style={{ marginTop: 0.5 }} />
23
+ </box>
24
+ );
25
+ }
@@ -0,0 +1,9 @@
1
+ import type { SelectOption } from "@opentui/core";
2
+ export interface SelectFieldProps {
3
+ label: string;
4
+ name: string;
5
+ options: SelectOption[];
6
+ required?: boolean;
7
+ onChange?: (value: string) => void;
8
+ }
9
+ export declare function SelectField({ label, name, options, required, onChange }: SelectFieldProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import type { SelectOption } from "@opentui/core";
2
+ import { useState } from "react";
3
+
4
+ export interface SelectFieldProps {
5
+ label: string;
6
+ name: string;
7
+ options: SelectOption[];
8
+ required?: boolean;
9
+ onChange?: (value: string) => void;
10
+ }
11
+
12
+ export function SelectField({ label, name, options, required, onChange }: SelectFieldProps) {
13
+ const [_selectedIndex, setSelectedIndex] = useState(0);
14
+
15
+ const handleChange = (index: number, option: SelectOption | null) => {
16
+ setSelectedIndex(index);
17
+ if (option) {
18
+ onChange?.(option.value);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <box style={{ flexDirection: "column", marginBottom: 1 }}>
24
+ <text content={`${label}${required ? " *" : ""}`} />
25
+ <box border height={8} style={{ marginTop: 0.5 }}>
26
+ <select focused={true} onChange={handleChange} options={options} />
27
+ </box>
28
+ </box>
29
+ );
30
+ }
@@ -0,0 +1,6 @@
1
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
2
+ export { useKeyboard, useOnResize, useRenderer, useTerminalDimensions, useTimeline, } from "@opentui/react";
3
+ export * from "./Form.js";
4
+ export * from "./FormField.js";
5
+ export * from "./ProgressBar.js";
6
+ export * from "./SelectField.js";
@@ -0,0 +1,12 @@
1
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
2
+ export {
3
+ useKeyboard,
4
+ useOnResize,
5
+ useRenderer,
6
+ useTerminalDimensions,
7
+ useTimeline
8
+ } from "@opentui/react";
9
+ export * from "./Form.js";
10
+ export * from "./FormField.js";
11
+ export * from "./ProgressBar.js";
12
+ export * from "./SelectField.js";
package/dist/mod.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type { CliRendererConfig, KeyEvent, SelectOption } from "@opentui/core";
2
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
3
+ export { useKeyboard, useOnResize, useRenderer, useTerminalDimensions, useTimeline, } from "@opentui/react";
4
+ export * from "./components/mod.js";
5
+ export { registerTuiRenderer } from "./renderer.js";
package/dist/mod.js ADDED
@@ -0,0 +1,10 @@
1
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
2
+ export {
3
+ useKeyboard,
4
+ useOnResize,
5
+ useRenderer,
6
+ useTerminalDimensions,
7
+ useTimeline
8
+ } from "@opentui/react";
9
+ export * from "./components/mod.js";
10
+ export { registerTuiRenderer } from "./renderer.js";
@@ -0,0 +1 @@
1
+ export { registerTuiRenderer as registerTui } from "./renderer.js";
@@ -0,0 +1 @@
1
+ export { registerTuiRenderer as registerTui } from "./renderer.js";
@@ -0,0 +1 @@
1
+ export declare function registerTuiRenderer(): void;
@@ -0,0 +1,24 @@
1
+ import { createCliRenderer } from "@opentui/core";
2
+ import { createRoot } from "@opentui/react";
3
+ import type { RenderArgs } from "@reliverse/rempts-core";
4
+ import { registerTuiRenderer as coreRegisterTuiRenderer } from "@reliverse/rempts-core";
5
+ import type { ReactElement } from "react";
6
+
7
+ export function registerTuiRenderer(): void {
8
+ coreRegisterTuiRenderer(async (args: RenderArgs<any, any>) => {
9
+ const component = args.command.render?.(args);
10
+
11
+ if (!component) {
12
+ throw new Error("TUI render result is missing. Ensure command.render returns JSX.");
13
+ }
14
+
15
+ const renderer = await createCliRenderer({
16
+ exitOnCtrlC: args.rendererOptions?.exitOnCtrlC ?? true,
17
+ targetFps: args.rendererOptions?.targetFps ?? 30,
18
+ enableMouseMovement: args.rendererOptions?.enableMouseMovement ?? true,
19
+ ...args.rendererOptions,
20
+ });
21
+
22
+ createRoot(renderer).render(component as ReactElement);
23
+ });
24
+ }
@@ -0,0 +1,9 @@
1
+ export interface TuiRendererOptions {
2
+ exitOnCtrlC?: boolean;
3
+ targetFps?: number;
4
+ enableMouseMovement?: boolean;
5
+ [key: string]: unknown;
6
+ }
7
+ export interface TuiConfig {
8
+ renderer?: TuiRendererOptions;
9
+ }
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@reliverse/rempts-tui",
3
+ "version": "2.3.1",
4
+ "description": "Terminal User Interface plugin for Rempts CLI framework",
5
+ "keywords": [
6
+ "cli",
7
+ "interactive",
8
+ "rempts",
9
+ "terminal",
10
+ "tui",
11
+ "ui"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "bun dler Contributors",
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "type": "module",
20
+ "main": "src/mod.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/mod.d.ts",
24
+ "default": "./dist/mod.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "@opentui/core": "^0.1.72",
29
+ "@opentui/react": "^0.1.72",
30
+ "@reliverse/rempts-core": "2.3.1"
31
+ },
32
+ "peerDependencies": {
33
+ "@opentui/core": "^0.1.72",
34
+ "react": "^19.2.3"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
@@ -0,0 +1,35 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { useCallback, useState } from "react";
3
+
4
+ export interface FormProps {
5
+ title: string;
6
+ onSubmit: (values: Record<string, any>) => void;
7
+ onCancel?: () => void;
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export function Form({ title, onSubmit, onCancel, children }: FormProps) {
12
+ const [formValues, setFormValues] = useState<Record<string, any>>({});
13
+
14
+ const _handleFieldChange = useCallback((name: string, value: any) => {
15
+ setFormValues((prev) => ({ ...prev, [name]: value }));
16
+ }, []);
17
+
18
+ useKeyboard((key) => {
19
+ if (key.name === "escape" && onCancel) {
20
+ onCancel();
21
+ }
22
+ if (key.name === "enter") {
23
+ onSubmit(formValues);
24
+ }
25
+ });
26
+
27
+ return (
28
+ <box border padding={2} style={{ flexDirection: "column" }} title={title}>
29
+ {children}
30
+ <box style={{ flexDirection: "row", gap: 2, marginTop: 2 }}>
31
+ <text content="Press Enter to submit, Esc to cancel" />
32
+ </box>
33
+ </box>
34
+ );
35
+ }
@@ -0,0 +1,47 @@
1
+ import { useState } from "react";
2
+
3
+ export interface FormFieldProps {
4
+ label: string;
5
+ name: string;
6
+ placeholder?: string;
7
+ required?: boolean;
8
+ value?: string;
9
+ onChange?: (value: string) => void;
10
+ onSubmit?: (value: string) => void;
11
+ }
12
+
13
+ export function FormField({
14
+ label,
15
+ name,
16
+ placeholder,
17
+ required,
18
+ value: initialValue = "",
19
+ onChange,
20
+ onSubmit,
21
+ }: FormFieldProps) {
22
+ const [_value, setValue] = useState(initialValue);
23
+
24
+ const handleInput = (newValue: string) => {
25
+ setValue(newValue);
26
+ onChange?.(newValue);
27
+ };
28
+
29
+ const handleSubmit = (submittedValue: string) => {
30
+ onSubmit?.(submittedValue);
31
+ };
32
+
33
+ return (
34
+ <box style={{ flexDirection: "column", marginBottom: 1 }}>
35
+ <text content={`${label}${required ? " *" : ""}`} />
36
+ <box border height={3} style={{ marginTop: 0.5 }} title={label}>
37
+ <input
38
+ focused={true}
39
+ onInput={handleInput}
40
+ onSubmit={handleSubmit}
41
+ placeholder={placeholder}
42
+ style={{ focusedBackgroundColor: "#000000" }}
43
+ />
44
+ </box>
45
+ </box>
46
+ );
47
+ }
@@ -0,0 +1,25 @@
1
+ export interface ProgressBarProps {
2
+ value: number; // 0-100
3
+ label?: string;
4
+ color?: string;
5
+ }
6
+
7
+ export function ProgressBar({ value, label, color = "#00ff00" }: ProgressBarProps) {
8
+ const clampedValue = Math.max(0, Math.min(100, value));
9
+
10
+ return (
11
+ <box style={{ flexDirection: "column" }}>
12
+ {label && <text content={label} />}
13
+ <box style={{ backgroundColor: "#333", height: 3, marginTop: 0.5 }}>
14
+ <box
15
+ style={{
16
+ width: `${clampedValue}%`,
17
+ backgroundColor: color,
18
+ height: 1,
19
+ }}
20
+ />
21
+ </box>
22
+ <text content={`${Math.floor(clampedValue)}%`} style={{ marginTop: 0.5 }} />
23
+ </box>
24
+ );
25
+ }
@@ -0,0 +1,30 @@
1
+ import type { SelectOption } from "@opentui/core";
2
+ import { useState } from "react";
3
+
4
+ export interface SelectFieldProps {
5
+ label: string;
6
+ name: string;
7
+ options: SelectOption[];
8
+ required?: boolean;
9
+ onChange?: (value: string) => void;
10
+ }
11
+
12
+ export function SelectField({ label, name, options, required, onChange }: SelectFieldProps) {
13
+ const [_selectedIndex, setSelectedIndex] = useState(0);
14
+
15
+ const handleChange = (index: number, option: SelectOption | null) => {
16
+ setSelectedIndex(index);
17
+ if (option) {
18
+ onChange?.(option.value);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <box style={{ flexDirection: "column", marginBottom: 1 }}>
24
+ <text content={`${label}${required ? " *" : ""}`} />
25
+ <box border height={8} style={{ marginTop: 0.5 }}>
26
+ <select focused={true} onChange={handleChange} options={options} />
27
+ </box>
28
+ </box>
29
+ );
30
+ }
@@ -0,0 +1,12 @@
1
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
2
+ export {
3
+ useKeyboard,
4
+ useOnResize,
5
+ useRenderer,
6
+ useTerminalDimensions,
7
+ useTimeline,
8
+ } from "@opentui/react";
9
+ export * from "./Form";
10
+ export * from "./FormField";
11
+ export * from "./ProgressBar";
12
+ export * from "./SelectField";
package/src/mod.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Export TUI renderer registration
2
+
3
+ // Re-export useful OpenTUI core types and utilities
4
+ export type { CliRendererConfig, KeyEvent, SelectOption } from "@opentui/core";
5
+ // Re-export text styling utilities
6
+ export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
7
+
8
+ // Re-export useful OpenTUI React hooks and types
9
+ export {
10
+ useKeyboard,
11
+ useOnResize,
12
+ useRenderer,
13
+ useTerminalDimensions,
14
+ useTimeline,
15
+ } from "@opentui/react";
16
+ // Export component library
17
+ export * from "./components/mod";
18
+ export { registerTuiRenderer } from "./renderer";
@@ -0,0 +1 @@
1
+ export { registerTuiRenderer as registerTui } from "./renderer";
@@ -0,0 +1,24 @@
1
+ import { createCliRenderer } from "@opentui/core";
2
+ import { createRoot } from "@opentui/react";
3
+ import type { RenderArgs } from "@reliverse/rempts-core";
4
+ import { registerTuiRenderer as coreRegisterTuiRenderer } from "@reliverse/rempts-core";
5
+ import type { ReactElement } from "react";
6
+
7
+ export function registerTuiRenderer(): void {
8
+ coreRegisterTuiRenderer(async (args: RenderArgs<any, any>) => {
9
+ const component = args.command.render?.(args);
10
+
11
+ if (!component) {
12
+ throw new Error("TUI render result is missing. Ensure command.render returns JSX.");
13
+ }
14
+
15
+ const renderer = await createCliRenderer({
16
+ exitOnCtrlC: args.rendererOptions?.exitOnCtrlC ?? true,
17
+ targetFps: args.rendererOptions?.targetFps ?? 30,
18
+ enableMouseMovement: args.rendererOptions?.enableMouseMovement ?? true,
19
+ ...args.rendererOptions,
20
+ });
21
+
22
+ createRoot(renderer).render(component as ReactElement);
23
+ });
24
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface TuiRendererOptions {
2
+ exitOnCtrlC?: boolean;
3
+ targetFps?: number;
4
+ enableMouseMovement?: boolean;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ export interface TuiConfig {
9
+ renderer?: TuiRendererOptions;
10
+ }