Ink UI

repository·main·Indexed 24 days ago

https://github.com/vadimdemedes/ink-ui

A collection of customizable, high-level UI components for building interactive Command Line Interfaces (CLIs) using the Ink framework. Version 2.0.0 includes components such as TextInput, EmailInput, PasswordInput, ConfirmInput, Select, MultiSelect, Spinner, ProgressBar, Badge, StatusMessage, Alert, and list components. It features a theming system using extendTheme and ThemeProvider to customize component styles and configurations.

Tokens
13.5K
Snippets
42
Records
63
Agent score
82%

What's inside @inkjs/ui

  1. Access component themes with useComponentTheme

    main

    If you are building a custom component and want it to participate in the Ink UI theming system, use the useComponentTheme hook. This allows your component to read styles and configuration defined in the current ThemeProvider.

    import React, {render, Text, type TextProps} from 'ink';
    import {
    	ThemeProvider,
    	defaultTheme,
    	extendTheme,
    	useComponentTheme,
    	type ComponentTheme,
    } from '@inkjs/ui';
    
    const customLabelTheme = {
    	styles: {
    		label: (): TextProps => ({
    			color: 'green',
    		}),
    	},
    } satisfies ComponentTheme;
    
    type CustomLabelTheme = typeof customLabelTheme;
    
    const customTheme = extendTheme(defaultTheme, {
    	components: {
    		CustomLabel: customLabelTheme,
    	},
    });
    
    function CustomLabel() {
    	const {styles} = useComponentTheme<CustomLabelTheme>('CustomLabel');
    
    	return <Text {...styles.label()}>Hello world</Text>;
    }
    
    function Example() {
    	return (
    		<ThemeProvider theme={customTheme}>
    			<CustomLabel />
    		</ThemeProvider>
    	);
    }
    
    render(<Example />);
  2. How to customize themes with extendTheme and ThemeProvider

    main

    All components in Ink UI use a theme accessible via React context. To customize the look of components, use extendTheme to merge the defaultTheme with your custom styles, and wrap your application in a ThemeProvider.

    Component themes consist of:

    • styles: Functions that return style objects (e.g., TextProps or BoxProps). These functions can accept component props to return conditional styles.
    • config: Configuration objects for non-styling properties (e.g., a list marker character).

    Example of customizing a Spinner to be magenta:

    import {render, type TextProps} from 'ink';
    import {Spinner, ThemeProvider, extendTheme, defaultTheme} from '@inkjs/ui';
    
    const customTheme = extendTheme(defaultTheme, {
    	components: {
    		Spinner: {
    			styles: {
    				frame: (): TextProps => ({
    					color: 'magenta',
    				}),
    			},
    		},
    	},
    });
    
    function Example() {
    	return (
    		<ThemeProvider theme={customTheme}>
    			<Spinner label="Loading" />
    		</ThemeProvider>
    	);
    }
    
    render(<Example />);
  3. Configure autocomplete with suggestions

    main

    You can provide an array of strings to the suggestions prop to enable autocomplete. As the user types, TextInput performs a case-sensitive search for the first item in the array that begins with the current input string. When the user presses <kbd>enter</kbd>, the matching suggestion replaces the current input value.

    import React, {useState} from 'react';
    import {render, Box, Text} from 'ink';
    import {TextInput} from '@inkjs/ui';
    
    function Example() {
    	const [value, setValue] = useState('');
    
    	return (
    		<Box flexDirection="column" gap={1}>
    			<TextInput
    				placeholder="Start typing..."
    				suggestions={['Abby', 'Angel', 'Annie']}
    				onChange={setValue}
    			/>
    
    			<Text>Input value: "{value}"</Text>
    		</Box>
    	);
    }
    
    render(<Example />);
  4. Use the ProgressBar component

    main

    The ProgressBar component is an extended version of the Spinner that allows you to visualize progress as a percentage. It is rendered within a container (like an Ink Box) to control its width.

    To use it, provide a value prop representing the current progress percentage.

    import React, {useEffect, useState} from 'react';
    import {render, Box} from 'ink';
    import {ProgressBar} from '@inkjs/ui';
    
    function Example() {
    	const [progress, setProgress] = useState(0);
    
    	useEffect(() => {
    		if (progress === 100) {
    			return;
    		}
    
    		const timer = setTimeout(() => {
    			setProgress(progress + 1);
    		}, 50);
    
    		return () => {
    			clearInterval(timer);
    		};
    	}, [progress]);
    
    	return (
    		<Box width={30}>
    			<ProgressBar value={progress} />
    		</Box>
    	);
    }
    
    render(<Example />);
  5. Use UnorderedList to display lists of items

    main

    The UnorderedList component is used to render lists of items in an Ink application. It follows a composite pattern where UnorderedList acts as the container and UnorderedList.Item represents individual entries. You can nest UnorderedList components inside an UnorderedList.Item to create nested lists.

    import React from 'react';
    import {render, Box, Text} from 'ink';
    import {UnorderedList} from '@inkjs/ui';
    
    function Example() {
    	return (
    		<UnorderedList>
    			<UnorderedList.Item>
    				<Text>Red</Text>
    			</UnorderedList.Item>
    
    			<UnorderedList.Item>
    				<Text>Green</Text>
    
    				<UnorderedList>
    					<UnorderedList.Item>
    						<Text>Light</Text>
    					</UnorderedList.Item>
    
    					<UnorderedList.Item>
    						<Text>Dark</Text>
    					</UnorderedList.Item>
    				</UnorderedList>
    			</UnorderedList.Item>
    
    			<UnorderedList.Item>
    				<Text>Blue</Text>
    			</UnorderedList.Item>
    		</UnorderedList>
    	);
    }
    
    render(<Example />);
  6. Use the OrderedList component

    main

    The OrderedList component is used to display lists of numbered items in a terminal UI. It follows a parent-child pattern where OrderedList acts as the container and OrderedList.Item represents each individual numbered entry. You can nest OrderedList components inside an OrderedList.Item to create sub-lists.

    import React from 'react';
    import {render, Box, Text} from 'ink';
    import {OrderedList} from '@inkjs/ui';
    
    function Example() {
    	return (
    		<OrderedList>
    			<OrderedList.Item>
    				<Text>Red</Text>
    			</OrderedList.Item>
    
    			<OrderedList.Item>
    				<Text>Green</Text>
    
    				<OrderedList>
    					<OrderedList.Item>
    						<Text>Light</Text>
    					</OrderedList.Item>
    
    					<OrderedList.Item>
    						<Text>Dark</Text>
    					</OrderedList.Item>
    				</OrderedList>
    			</OrderedList.Item>
    
    			<OrderedList.Item>
    				<Text>Blue</Text>
    			</OrderedList.Item>
    		</OrderedList>
    	);
    }
    
    render(<Example />);
  7. Use the EmailInput component

    main

    EmailInput is an uncontrolled component used for entering email addresses. It features automatic domain suggestions once the @ character is typed. You can track changes in real-time using the onChange prop or capture the final value when the user presses <kbd>enter</kbd> using the onSubmit prop.

    import React, {useState} from 'react';
    import {render, Box, Text} from 'ink';
    import {EmailInput} from '@inkjs/ui';
    
    function Example() {
    	const [value, setValue] = useState('');
    
    	return (
    		<Box flexDirection="column" gap={1}>
    			<EmailInput placeholder="Enter email..." onChange={setValue} />
    			<Text>Input value: "{value}"</Text>
    		</Box>
    	);
    }
    
    render(<Example />);
  8. Use the PasswordInput component

    main

    The PasswordInput component is used for entering sensitive data like passwords or API keys. It masks the input value with asterisks (*). It is an uncontrolled component, meaning you manage its state by listening to value changes via the onChange or onSubmit props.

    import React, {useState} from 'react';
    import {render, Box, Text} from 'ink';
    import {PasswordInput} from '@inkjs/ui';
    
    function Example() {
    	const [value, setValue] = useState('');
    
    	return (
    		<Box flexDirection="column" gap={1}>
    			<PasswordInput placeholder="Enter password..." onChange={setValue} />
    			<Text>Input value: "{value}"</Text>
    		</Box>
    	);
    }
    
    render(<Example />);
  9. Capture input on Enter with onSubmit

    main

    If you only need the final value once the user has finished typing and presses <kbd>enter</kbd>, use the onSubmit prop instead of onChange. This is useful for form submissions or sequential input flows.

    import React, {useState} from 'react';
    import {render, Box, Text} from 'ink';
    import {TextInput} from '@inkjs/ui';
    
    function Example() {
    	const [value, setValue] = useState('');
    
    	return (
    		<Box flexDirection="column" gap={1}>
    			<TextInput placeholder="Start typing..." onSubmit={setValue} />
    			<Text>Input value: "{value}"</Text>
    		</Box>
    	);
    }
    
    render(<Example />);
  10. Handle password submission with onSubmit

    main

    If you only need the final value once the user presses <kbd>enter</kbd>, use the onSubmit prop instead of onChange. This prevents continuous updates during typing and only triggers the callback when the input is submitted.

    import React, {useState} from 'react';
    import {render, Box, Text} from 'ink';
    import {PasswordInput} from '@inkjs/ui';
    
    function Example() {
    	const [value, setValue] = useState('');
    
    	return (
    		<Box flexDirection="column" gap={1}>
    			<PasswordInput placeholder="Enter password..." onSubmit={setValue} />
    			<Text>Input value: "{value}"</Text>
    		</Box>
    	);
    }
    
    render(<Example />);
  11. Use the StatusMessage component

    main

    The StatusMessage component is used to display status indicators, particularly when a longer explanation of a status is required. You can customize the visual style of the message using the variant prop to change its color based on the status type.

    import React from 'react';
    import {render, Box} from 'ink';
    import {StatusMessage} from '@inkjs/ui';
    
    function Example() {
    	return (
    		<Box flexDirection="column" padding={2}>
    			<StatusMessage variant="success">Success</StatusMessage>
    			<StatusMessage variant="error">Error</StatusMessage>
    			<StatusMessage variant="warning">Warning</StatusMessage>
    			<StatusMessage variant="info">Info</StatusMessage>
    		</Box>
    	);
    }
    
    render(<Example />);