├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── docs ├── alert.md ├── badge.md ├── confirm-input.md ├── email-input.md ├── multi-select.md ├── ordered-list.md ├── password-input.md ├── progress-bar.md ├── select.md ├── spinner.md ├── status-message.md ├── text-input.md └── unordered-list.md ├── examples ├── alert.tsx ├── autocomplete.tsx ├── badge.tsx ├── confirm-input.tsx ├── email-input.tsx ├── multi-select.tsx ├── ordered-list.tsx ├── password-input.tsx ├── progress-bar.tsx ├── select.tsx ├── spinner.tsx ├── status-message.tsx ├── text-input.tsx ├── theming │ ├── custom-component.tsx │ ├── spinner.tsx │ └── unordered-list.tsx └── unordered-list.tsx ├── license ├── media ├── alert.png ├── badge.png ├── confirm-input.png ├── email-input-autocomplete.gif ├── email-input-basic.gif ├── email-input-default-value.gif ├── email-input-disabled.gif ├── email-input-submit.gif ├── email-input.gif ├── multi-select-basic.gif ├── multi-select-default-value.gif ├── multi-select-disabled.gif ├── multi-select-submit.gif ├── multi-select.gif ├── ordered-list.png ├── password-input-basic.gif ├── password-input-disabled.gif ├── password-input-submit.gif ├── password-input.gif ├── progress-bar.gif ├── select-basic.gif ├── select-default-value.gif ├── select-disabled.gif ├── select.gif ├── source │ ├── confirm-input │ │ ├── basic.tsx │ │ └── readme.tsx │ ├── email-input │ │ ├── autocomplete.tsx │ │ ├── basic.tsx │ │ ├── default-value.tsx │ │ ├── disabled.tsx │ │ ├── readme.tsx │ │ └── submit.tsx │ ├── helpers │ │ ├── escapes.ts │ │ ├── input.ts │ │ └── press.ts │ ├── multi-select │ │ ├── basic.tsx │ │ ├── default-value.tsx │ │ ├── disabled.tsx │ │ ├── readme.tsx │ │ └── submit.tsx │ ├── password-input │ │ ├── basic.tsx │ │ ├── disabled.tsx │ │ ├── readme.tsx │ │ └── submit.tsx │ ├── select │ │ ├── basic.tsx │ │ ├── default-value.tsx │ │ ├── disabled.tsx │ │ └── readme.tsx │ └── text-input │ │ ├── autocomplete.tsx │ │ ├── basic.tsx │ │ ├── default-value.tsx │ │ ├── disabled.tsx │ │ ├── readme.tsx │ │ └── submit.tsx ├── spinner-theme.gif ├── spinner.gif ├── status-message.png ├── text-input-autocomplete.gif ├── text-input-basic.gif ├── text-input-default-value.gif ├── text-input-disabled.gif ├── text-input-submit.gif ├── text-input.gif ├── unordered-list-theme.png └── unordered-list.png ├── package.json ├── readme.md ├── source ├── components │ ├── alert │ │ ├── alert.tsx │ │ ├── index.ts │ │ └── theme.ts │ ├── badge │ │ ├── badge.tsx │ │ ├── index.ts │ │ └── theme.ts │ ├── confirm-input │ │ ├── confirm-input.tsx │ │ ├── index.ts │ │ └── theme.ts │ ├── email-input │ │ ├── email-input.tsx │ │ ├── index.ts │ │ ├── theme.ts │ │ ├── use-email-input-state.ts │ │ └── use-email-input.ts │ ├── multi-select │ │ ├── index.ts │ │ ├── multi-select-option.tsx │ │ ├── multi-select.tsx │ │ ├── theme.ts │ │ ├── use-multi-select-state.ts │ │ └── use-multi-select.ts │ ├── ordered-list │ │ ├── index.ts │ │ ├── ordered-list-context.ts │ │ ├── ordered-list-item-context.ts │ │ ├── ordered-list-item.tsx │ │ ├── ordered-list.tsx │ │ └── theme.ts │ ├── password-input │ │ ├── index.ts │ │ ├── password-input.tsx │ │ ├── theme.ts │ │ ├── use-password-input-state.ts │ │ └── use-password-input.ts │ ├── progress-bar │ │ ├── index.ts │ │ ├── progress-bar.tsx │ │ └── theme.ts │ ├── select │ │ ├── index.ts │ │ ├── select-option.tsx │ │ ├── select.tsx │ │ ├── theme.ts │ │ ├── use-select-state.ts │ │ └── use-select.ts │ ├── spinner │ │ ├── index.ts │ │ ├── spinner.tsx │ │ ├── theme.ts │ │ └── use-spinner.ts │ ├── status-message │ │ ├── index.ts │ │ ├── status-message.tsx │ │ ├── theme.ts │ │ └── types.ts │ ├── text-input │ │ ├── index.ts │ │ ├── text-input.tsx │ │ ├── theme.ts │ │ ├── use-text-input-state.ts │ │ └── use-text-input.ts │ └── unordered-list │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── theme.ts │ │ ├── unordered-list-context.ts │ │ ├── unordered-list-item-context.ts │ │ ├── unordered-list-item.tsx │ │ └── unordered-list.tsx ├── index.ts ├── lib │ └── option-map.ts ├── theme.tsx └── types.ts ├── test ├── alert.tsx ├── badge.tsx ├── confirm-input.tsx ├── email-input.tsx ├── multi-select.tsx ├── ordered-list.tsx ├── password-input.tsx ├── progress-bar.tsx ├── select.tsx ├── spinner.tsx ├── status-message.tsx ├── text-input.tsx └── unordered-list.tsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 18 13 | - run: npm install 14 | - run: npm test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .tsimp 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /docs/alert.md: -------------------------------------------------------------------------------- 1 | # Alert 2 | 3 | > `Alert` is used to focus user's attention to important messages. 4 | 5 | [Theme](../source/components/alert/theme.ts) | [Example code](../examples/alert.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box} from 'ink'; 12 | import {Alert} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ( 16 | 17 | A new version of this CLI is available 18 | 19 | Your license is expired 20 | 21 | 22 | Current version of this CLI has been deprecated 23 | 24 | 25 | API won't be available tomorrow night 26 | 27 | ); 28 | } 29 | 30 | render(); 31 | ``` 32 | 33 | 34 | 35 | ## Props 36 | 37 | ### children 38 | 39 | Type: `ReactNode` 40 | 41 | Message. 42 | 43 | ### variant 44 | 45 | Type: `'info' | 'success' | 'error' | 'warning'` 46 | 47 | Variant, which determines the color of the alert. 48 | 49 | ### title 50 | 51 | Type: `string` 52 | 53 | Title to show above the message. 54 | -------------------------------------------------------------------------------- /docs/badge.md: -------------------------------------------------------------------------------- 1 | # Badge 2 | 3 | > `Badge` can be used to indicate a status of a certain item, usually positioned nearby the element it's related to. 4 | 5 | [Theme](../source/components/badge/theme.ts) | [Example code](../examples/badge.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box} from 'ink'; 12 | import {Badge} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ( 16 | 17 | Pass 18 | Fail 19 | Warn 20 | Todo 21 | 22 | ); 23 | } 24 | 25 | render(); 26 | ``` 27 | 28 | 29 | 30 | ## Props 31 | 32 | ### children 33 | 34 | Type: `ReactNode` 35 | 36 | Label. 37 | 38 | ### color 39 | 40 | Type: [`TextProps['color']`](https://github.com/vadimdemedes/ink#color) 41 | 42 | Color. 43 | -------------------------------------------------------------------------------- /docs/confirm-input.md: -------------------------------------------------------------------------------- 1 | # Confirm input 2 | 3 | > `ConfirmInput` shows a common "Y/n" input to confirm or cancel an operation your CLI wants to perform. 4 | 5 | [Theme](../source/components/confirm-input/theme.ts) | [Example code](../examples/confirm-input.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React, {useState} from 'react'; 11 | import {render, Box, Text} from 'ink'; 12 | import {ConfirmInput} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | const [choice, setChoice] = useState<'agreed' | 'disagreed' | undefined>(); 16 | 17 | return ( 18 | 19 | {!choice && ( 20 | <> 21 | Do you agree with terms of service? 22 | { 24 | setChoice('agreed'); 25 | }} 26 | onCancel={() => { 27 | setChoice('disagreed'); 28 | }} 29 | /> 30 | 31 | )} 32 | 33 | {choice === 'agreed' && I know you haven't read them, but ok} 34 | {choice === 'disagreed' && Ok, whatever} 35 | 36 | ); 37 | } 38 | 39 | render(); 40 | ``` 41 | 42 | ## Props 43 | 44 | ### isDisabled 45 | 46 | Type: `boolean`\ 47 | Default: `false` 48 | 49 | When disabled, user input is ignored. 50 | 51 | ### defaultChoice 52 | 53 | Type: `'confirm' | 'cancel'`\ 54 | Default: `'confirm'` 55 | 56 | Default choice. 57 | 58 | ### submitOnEnter 59 | 60 | Type: `boolean`\ 61 | Default: `true` 62 | 63 | Confirm or cancel when user presses enter, depending on the `defaultChoice` value. 64 | Can be useful to disable when an explicit confirmation is required, such as pressing Y key. 65 | 66 | ### onConfirm 67 | 68 | Type: `Function` 69 | 70 | Callback to trigger on confirmation. 71 | 72 | ### onCancel 73 | 74 | Type: `Function` 75 | 76 | Callback to trigger on cancellation. 77 | -------------------------------------------------------------------------------- /docs/email-input.md: -------------------------------------------------------------------------------- 1 | # Email input 2 | 3 | > `EmailInput` is used for entering an email. After "@" character is entered, domain can be autocompleted from the list of most popular email providers. 4 | 5 | [Theme](../source/components/email-input/theme.ts) | [Example code](../examples/email-input.tsx) 6 | 7 | ## Usage 8 | 9 | `EmailInput` is an uncontrolled component. You can listen to value changes via `onChange` prop. 10 | 11 | ```tsx 12 | import React, {useState} from 'react'; 13 | import {render, Box, Text} from 'ink'; 14 | import {EmailInput} from '@inkjs/ui'; 15 | 16 | function Example() { 17 | const [value, setValue] = useState(''); 18 | 19 | return ( 20 | 21 | 22 | Input value: "{value}" 23 | 24 | ); 25 | } 26 | 27 | render(); 28 | ``` 29 | 30 | 31 | 32 | ### Default value 33 | 34 | Default value can be set via `defaultValue` prop. 35 | 36 | ```tsx 37 | import React, {useState} from 'react'; 38 | import {render, Box, Text} from 'ink'; 39 | import {EmailInput} from '@inkjs/ui'; 40 | 41 | function Example() { 42 | const [value, setValue] = useState('jane@hey.com'); 43 | 44 | return ( 45 | 46 | 51 | 52 | Input value: "{value}" 53 | 54 | ); 55 | } 56 | 57 | render(); 58 | ``` 59 | 60 | 61 | 62 | ### Autocomplete 63 | 64 | `EmailInput` suggests the domains of popular email providers once "@" character is entered. You can also customize the list of suggested domains by providing an array of strings in `domains` prop. 65 | 66 | When user presses enter, current suggestion will replace the input value. 67 | 68 | ```tsx 69 | import React, {useState} from 'react'; 70 | import {render, Box, Text} from 'ink'; 71 | import {EmailInput} from '@inkjs/ui'; 72 | 73 | function Example() { 74 | const [value, setValue] = useState(''); 75 | 76 | return ( 77 | 78 | 83 | 84 | Input value: "{value}" 85 | 86 | ); 87 | } 88 | 89 | render(); 90 | ``` 91 | 92 | 93 | 94 | ### Submit on enter 95 | 96 | When you're only looking for the final value when user presses enter, you can use `onSubmit` instead of `onChange` prop. 97 | 98 | ```tsx 99 | import React, {useState} from 'react'; 100 | import {render, Box, Text} from 'ink'; 101 | import {EmailInput} from '@inkjs/ui'; 102 | 103 | function Example() { 104 | const [value, setValue] = useState(''); 105 | 106 | return ( 107 | 108 | 109 | Input value: "{value}" 110 | 111 | ); 112 | } 113 | 114 | render(); 115 | ``` 116 | 117 | 118 | 119 | ### Disabled 120 | 121 | When there are two or more text inputs, only one should be receiving user input at a time, while others should be disabled via `isDisabled` prop. 122 | 123 | ```tsx 124 | import React, {useState} from 'react'; 125 | import {render, Box, Text} from 'ink'; 126 | import {EmailInput} from '@inkjs/ui'; 127 | 128 | function Example() { 129 | const [activeInput, setActiveInput] = useState('first'); 130 | const [first, setFirst] = useState(''); 131 | const [second, setSecond] = useState(''); 132 | 133 | return ( 134 | 135 | 136 | { 141 | setActiveInput('second'); 142 | }} 143 | /> 144 | 145 | { 150 | setActiveInput('none'); 151 | }} 152 | /> 153 | 154 | 155 | 156 | First email: "{first}" 157 | Second email: "{second}" 158 | 159 | 160 | ); 161 | } 162 | 163 | render(); 164 | ``` 165 | 166 | 167 | 168 | ## Props 169 | 170 | ### isDisabled 171 | 172 | Type: `boolean`\ 173 | Default: `false` 174 | 175 | When disabled, user input is ignored. 176 | 177 | ### placeholder 178 | 179 | Type: `string` 180 | 181 | Text to display when input is empty. 182 | 183 | ### defaultValue 184 | 185 | Type: `string` 186 | 187 | Default input value. 188 | 189 | ### domains 190 | 191 | Type: `string[]`\ 192 | Default: `["aol.com", "gmail.com", "yahoo.com", "hotmail.com", "live.com", "outlook.com", "icloud.com", "hey.com"]` 193 | 194 | Domains of email providers to autocomplete. 195 | 196 | ### onChange(value) 197 | 198 | Type: `Function` 199 | 200 | Callback when input value changes. 201 | 202 | #### value 203 | 204 | Type: `string` 205 | 206 | Input value. 207 | 208 | ### onSubmit(value) 209 | 210 | Type: `Function` 211 | 212 | Callback when enter is pressed. 213 | 214 | #### value 215 | 216 | Type: `string` 217 | 218 | Input value. 219 | -------------------------------------------------------------------------------- /docs/multi-select.md: -------------------------------------------------------------------------------- 1 | # Multi select 2 | 3 | > `MultiSelect` is similar to [`Select`](select.md), except user can choose multiple options. 4 | 5 | [Theme](../source/components/multi-select/theme.ts) | [Example code](../examples/multi-select.tsx) 6 | 7 | ## Usage 8 | 9 | `MultiSelect` is an uncontrolled component. You can listen to value changes via `onChange` prop. 10 | 11 | ```tsx 12 | import React, {useState} from 'react'; 13 | import {render, Box, Text} from 'ink'; 14 | import {MultiSelect} from '@inkjs/ui'; 15 | 16 | function Example() { 17 | const [value, setValue] = useState([]); 18 | 19 | return ( 20 | 21 | 54 | 55 | Selected values: {value.join(', ')} 56 | 57 | ); 58 | } 59 | 60 | render(); 61 | ``` 62 | 63 | 64 | 65 | ### Default value 66 | 67 | Default value can be set via `defaultValue` prop. 68 | 69 | ```tsx 70 | import React, {useState} from 'react'; 71 | import {render, Box, Text} from 'ink'; 72 | import {MultiSelect} from '@inkjs/ui'; 73 | 74 | function Example() { 75 | const [value, setValue] = useState(['green']); 76 | 77 | return ( 78 | 79 | 113 | 114 | Selected values: {value.join(', ')} 115 | 116 | ); 117 | } 118 | 119 | render(); 120 | ``` 121 | 122 | 123 | 124 | ### Submit on enter 125 | 126 | When you're only looking for the final value when user presses enter, you can use `onSubmit` instead of `onChange` prop. 127 | 128 | ```tsx 129 | import React, {useState} from 'react'; 130 | import {render, Box, Text} from 'ink'; 131 | import {MultiSelect} from '@inkjs/ui'; 132 | 133 | function Example() { 134 | const [value, setValue] = useState([]); 135 | 136 | return ( 137 | 138 | 171 | 172 | Selected values: {value.join(', ')} 173 | 174 | ); 175 | } 176 | 177 | render(); 178 | ``` 179 | 180 | 181 | 182 | ### Disabled 183 | 184 | When there are two or more selects, only one should be receiving user input at a time, while others should be disabled via `isDisabled` prop. 185 | 186 | ```tsx 187 | import React, {useState} from 'react'; 188 | import {render, Box, Text} from 'ink'; 189 | import {MultiSelect} from '@inkjs/ui'; 190 | 191 | const options = [ 192 | { 193 | label: 'Red', 194 | value: 'red', 195 | }, 196 | { 197 | label: 'Green', 198 | value: 'green', 199 | }, 200 | { 201 | label: 'Yellow', 202 | value: 'yellow', 203 | }, 204 | { 205 | label: 'Blue', 206 | value: 'blue', 207 | }, 208 | { 209 | label: 'Magenta', 210 | value: 'magenta', 211 | }, 212 | { 213 | label: 'Cyan', 214 | value: 'cyan', 215 | }, 216 | { 217 | label: 'White', 218 | value: 'white', 219 | }, 220 | ]; 221 | 222 | function Example() { 223 | const [activeInput, setActiveInput] = useState('primary'); 224 | const [primaryColors, setPrimaryColors] = useState([]); 225 | const [secondaryColors, setSecondaryColors] = useState([]); 226 | 227 | return ( 228 | 229 | 230 | { 235 | setActiveInput('secondary'); 236 | }} 237 | /> 238 | 239 | Primary colors: {primaryColors.join(', ')} 240 | 241 | 242 | 243 | { 248 | setActiveInput('none'); 249 | }} 250 | /> 251 | 252 | Secondary colors: {secondaryColors.join(', ')} 253 | 254 | 255 | ); 256 | } 257 | 258 | render(); 259 | ``` 260 | 261 | 262 | 263 | ## Props 264 | 265 | ### isDisabled 266 | 267 | Type: `boolean`\ 268 | Default: `false` 269 | 270 | When disabled, user input is ignored. 271 | 272 | ### visibleOptionCount 273 | 274 | Type: `number`\ 275 | Default: `5` 276 | 277 | Number of visible options. 278 | 279 | ### highlightText 280 | 281 | Type: `string` 282 | 283 | Highlight text in option labels. 284 | 285 | ### options 286 | 287 | Type: `Array<{ label: string; value: string; }>` 288 | 289 | Options. 290 | 291 | ### defaultValue 292 | 293 | Type: `string[]` 294 | 295 | Default value. 296 | 297 | ### onChange(value) 298 | 299 | Type: `Function` 300 | 301 | Callback when selected options change. 302 | 303 | #### value 304 | 305 | Type: `string[]` 306 | 307 | Values of selected options. 308 | 309 | ### onSubmit(value) 310 | 311 | Type: `Function` 312 | 313 | Callback when user presses enter. 314 | 315 | #### value 316 | 317 | Type: `string[]` 318 | 319 | Value of selected options. 320 | -------------------------------------------------------------------------------- /docs/ordered-list.md: -------------------------------------------------------------------------------- 1 | # Ordered list 2 | 3 | > `OrderedList` is used to show lists of numbered items. 4 | 5 | [Theme](../source/components/ordered-list/theme.ts) | [Example code](../examples/ordered-list.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box, Text} from 'ink'; 12 | import {OrderedList} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ( 16 | 17 | 18 | Red 19 | 20 | 21 | 22 | Green 23 | 24 | 25 | 26 | Light 27 | 28 | 29 | 30 | Dark 31 | 32 | 33 | 34 | 35 | 36 | Blue 37 | 38 | 39 | ); 40 | } 41 | 42 | render(); 43 | ``` 44 | 45 | 46 | 47 | ## Props 48 | 49 | ### OrderedList 50 | 51 | #### children 52 | 53 | Type: `ReactNode` 54 | 55 | List items. 56 | 57 | ### OrderedList.Item 58 | 59 | #### children 60 | 61 | Type: `ReactNode` 62 | 63 | List item content. 64 | -------------------------------------------------------------------------------- /docs/password-input.md: -------------------------------------------------------------------------------- 1 | # Password input 2 | 3 | > `PasswordInput` is used for entering sensitive data, like passwords, API keys and so on. It works the same way as `PasswordInput`, except input value is masked and replaced with asterisks ("\*"). 4 | 5 | [Theme](../source/components/password-input/theme.ts) | [Example code](../examples/password-input.tsx) 6 | 7 | ## Usage 8 | 9 | `PasswordInput` is an uncontrolled component. You can listen to value changes via `onChange` prop. 10 | 11 | ```tsx 12 | import React, {useState} from 'react'; 13 | import {render, Box, Text} from 'ink'; 14 | import {PasswordInput} from '@inkjs/ui'; 15 | import input from '../helpers/input.js'; 16 | 17 | function Example() { 18 | const [value, setValue] = useState(''); 19 | 20 | return ( 21 | 22 | 23 | Input value: "{value}" 24 | 25 | ); 26 | } 27 | 28 | render(); 29 | ``` 30 | 31 | 32 | 33 | ### Submit on enter 34 | 35 | When you're only looking for the final value when user presses enter, you can use `onSubmit` instead of `onChange` prop. 36 | 37 | ```tsx 38 | import React, {useState} from 'react'; 39 | import {render, Box, Text} from 'ink'; 40 | import {PasswordInput} from '@inkjs/ui'; 41 | 42 | function Example() { 43 | const [value, setValue] = useState(''); 44 | 45 | return ( 46 | 47 | 48 | Input value: "{value}" 49 | 50 | ); 51 | } 52 | 53 | render(); 54 | ``` 55 | 56 | 57 | 58 | ### Disabled 59 | 60 | When there are two or more password inputs, only one should be receiving user input at a time, while others should be disabled via `isDisabled` prop. 61 | 62 | ```tsx 63 | import React, {useState} from 'react'; 64 | import {render, Box, Text} from 'ink'; 65 | import {PasswordInput} from '@inkjs/ui'; 66 | import input from '../helpers/input.js'; 67 | import press from '../helpers/press.js'; 68 | 69 | function Example() { 70 | const [activeInput, setActiveInput] = useState('password'); 71 | const [password, setPassword] = useState(''); 72 | const [passwordConfirmation, setPasswordConfirmation] = useState(''); 73 | 74 | return ( 75 | 76 | 77 | { 82 | setActiveInput('passwordConfirmation'); 83 | }} 84 | /> 85 | 86 | { 91 | setActiveInput('none'); 92 | }} 93 | /> 94 | 95 | 96 | 97 | Password: "{password}" 98 | Password confirmation: "{passwordConfirmation}" 99 | 100 | 101 | ); 102 | } 103 | 104 | render(); 105 | ``` 106 | 107 | 108 | 109 | ## Props 110 | 111 | ### isDisabled 112 | 113 | Type: `boolean`\ 114 | Default: `false` 115 | 116 | When disabled, user input is ignored. 117 | 118 | ### placeholder 119 | 120 | Type: `string` 121 | 122 | Text to display when input is empty. 123 | 124 | ### onChange(value) 125 | 126 | Type: `Function` 127 | 128 | Callback when input value changes. 129 | 130 | #### value 131 | 132 | Type: `string` 133 | 134 | Input value. 135 | 136 | ### onSubmit(value) 137 | 138 | Type: `Function` 139 | 140 | Callback when enter is pressed. 141 | 142 | #### value 143 | 144 | Type: `string` 145 | 146 | Input value. 147 | -------------------------------------------------------------------------------- /docs/progress-bar.md: -------------------------------------------------------------------------------- 1 | # Progress bar 2 | 3 | > `ProgressBar` is an extended version of [`Spinner`](spinner.md), where it's possible to calculate a progress percentage. 4 | 5 | [Theme](../source/components/progress-bar/theme.ts) | [Example code](../examples/progress-bar.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React, {useEffect, useState} from 'react'; 11 | import {render, Box} from 'ink'; 12 | import {ProgressBar} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | const [progress, setProgress] = useState(0); 16 | 17 | useEffect(() => { 18 | if (progress === 100) { 19 | return; 20 | } 21 | 22 | const timer = setTimeout(() => { 23 | setProgress(progress + 1); 24 | }, 50); 25 | 26 | return () => { 27 | clearInterval(timer); 28 | }; 29 | }, [progress]); 30 | 31 | return ( 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | render(); 39 | ``` 40 | 41 | 42 | 43 | ## Props 44 | 45 | ### value 46 | 47 | Type: `number` \ 48 | Minimum: `0` \ 49 | Maximum: `100` \ 50 | Default: `0` 51 | 52 | Progress. 53 | -------------------------------------------------------------------------------- /docs/select.md: -------------------------------------------------------------------------------- 1 | # Select 2 | 3 | > `Select` shows a scrollable list of options for a user to choose from. 4 | 5 | [Theme](../source/components/select/theme.ts) | [Example code](../examples/select.tsx) 6 | 7 | ## Usage 8 | 9 | `Select` is an uncontrolled component. You can listen to value changes via `onChange` prop. 10 | 11 | ```tsx 12 | import React, {useState} from 'react'; 13 | import {render, Box, Text} from 'ink'; 14 | import {Select} from '@inkjs/ui'; 15 | 16 | function Example() { 17 | const [value, setValue] = useState(); 18 | 19 | return ( 20 | 21 | 113 | 114 | Selected value: {value} 115 | 116 | ); 117 | } 118 | 119 | render(); 120 | ``` 121 | 122 | 123 | 124 | ### Disabled 125 | 126 | When there are two or more selects, only one should be receiving user input at a time, while others should be disabled via `isDisabled` prop. 127 | 128 | ```tsx 129 | import React, {useState} from 'react'; 130 | import {render, Box, Text} from 'ink'; 131 | import {Select} from '@inkjs/ui'; 132 | 133 | const options = [ 134 | { 135 | label: 'Red', 136 | value: 'red', 137 | }, 138 | { 139 | label: 'Green', 140 | value: 'green', 141 | }, 142 | { 143 | label: 'Yellow', 144 | value: 'yellow', 145 | }, 146 | { 147 | label: 'Blue', 148 | value: 'blue', 149 | }, 150 | { 151 | label: 'Magenta', 152 | value: 'magenta', 153 | }, 154 | { 155 | label: 'Cyan', 156 | value: 'cyan', 157 | }, 158 | { 159 | label: 'White', 160 | value: 'white', 161 | }, 162 | ]; 163 | 164 | function Example() { 165 | const [activeInput, setActiveInput] = useState('primary'); 166 | const [primaryColor, setPrimaryColor] = useState(); 167 | const [secondaryColor, setSecondaryColor] = useState(); 168 | 169 | return ( 170 | 171 | 172 | { 189 | setSecondaryColor(value); 190 | setActiveInput('none'); 191 | }} 192 | /> 193 | 194 | Secondary color: {secondaryColor} 195 | 196 | 197 | ); 198 | } 199 | 200 | render(); 201 | ``` 202 | 203 | 204 | 205 | ## Props 206 | 207 | ### isDisabled 208 | 209 | Type: `boolean`\ 210 | Default: `false` 211 | 212 | When disabled, user input is ignored. 213 | 214 | ### visibleOptionCount 215 | 216 | Type: `number`\ 217 | Default: `5` 218 | 219 | Number of visible options. 220 | 221 | ### highlightText 222 | 223 | Type: `string` 224 | 225 | Highlight text in option labels. 226 | 227 | ### options 228 | 229 | Type: `Array<{ label: string; value: string; }>` 230 | 231 | Options. 232 | 233 | ### defaultValue 234 | 235 | Type: `string` 236 | 237 | Default value. 238 | 239 | ### onChange(value) 240 | 241 | Type: `Function` 242 | 243 | Callback when selected option changes. 244 | 245 | #### value 246 | 247 | Type: `string` 248 | 249 | Value of the selected option. 250 | -------------------------------------------------------------------------------- /docs/spinner.md: -------------------------------------------------------------------------------- 1 | # Spinner 2 | 3 | > `Spinner` indicates that something is being processed and CLI is waiting for it to complete. 4 | 5 | [Theme](../source/components/spinner/theme.ts) | [Example code](../examples/spinner.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box} from 'ink'; 12 | import {Spinner} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ; 16 | } 17 | 18 | render(); 19 | ``` 20 | 21 | 22 | 23 | ## Props 24 | 25 | ### label 26 | 27 | Type: `string` 28 | 29 | Label to show next to the spinner. 30 | -------------------------------------------------------------------------------- /docs/status-message.md: -------------------------------------------------------------------------------- 1 | # Status message 2 | 3 | > `StatusMessage` can also be used to indicate a status, but when longer explanation of such status is required. 4 | 5 | [Theme](../source/components/status-message/theme.ts) | [Example code](../examples/status-message.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box} from 'ink'; 12 | import {StatusMessage} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ( 16 | 17 | Success 18 | Error 19 | Warning 20 | Info 21 | 22 | ); 23 | } 24 | 25 | render(); 26 | ``` 27 | 28 | 29 | 30 | ## Props 31 | 32 | ### children 33 | 34 | Type: `ReactNode` 35 | 36 | Message. 37 | 38 | ### variant 39 | 40 | Type: `'info' | 'success' | 'error' | 'warning'` 41 | 42 | Variant, which determines the color used in the status message. 43 | -------------------------------------------------------------------------------- /docs/text-input.md: -------------------------------------------------------------------------------- 1 | # Text input 2 | 3 | > `TextInput` is used for entering any single-line input with an optional autocomplete. 4 | 5 | [Theme](../source/components/text-input/theme.ts) | [Example code](../examples/text-input.tsx) 6 | 7 | ## Usage 8 | 9 | `TextInput` is an uncontrolled component. You can listen to value changes via `onChange` prop. 10 | 11 | ```tsx 12 | import React, {useState} from 'react'; 13 | import {render, Box, Text} from 'ink'; 14 | import {TextInput} from '@inkjs/ui'; 15 | 16 | function Example() { 17 | const [value, setValue] = useState(''); 18 | 19 | return ( 20 | 21 | 22 | Input value: "{value}" 23 | 24 | ); 25 | } 26 | 27 | render(); 28 | ``` 29 | 30 | 31 | 32 | ### Default value 33 | 34 | Default value can be set via `defaultValue` prop. 35 | 36 | ```tsx 37 | import React, {useState} from 'react'; 38 | import {render, Box, Text} from 'ink'; 39 | import {TextInput} from '@inkjs/ui'; 40 | 41 | function Example() { 42 | const [value, setValue] = useState('Jane'); 43 | 44 | return ( 45 | 46 | 51 | 52 | Input value: "{value}" 53 | 54 | ); 55 | } 56 | 57 | render(); 58 | ``` 59 | 60 | 61 | 62 | ### Autocomplete 63 | 64 | `TextInput` can suggest values to autocomplete input with via `suggestions` prop, which contains an array of strings. 65 | As user is typing, `TextInput` finds the first item in `suggestions` array that begins with an input string that user has already entered. Matching of items in `suggestions` is case-sensitive. 66 | 67 | When user presses enter, current suggestion will replace the input value. 68 | 69 | ```tsx 70 | import React, {useState} from 'react'; 71 | import {render, Box, Text} from 'ink'; 72 | import {TextInput} from '@inkjs/ui'; 73 | 74 | function Example() { 75 | const [value, setValue] = useState(''); 76 | 77 | return ( 78 | 79 | 84 | 85 | Input value: "{value}" 86 | 87 | ); 88 | } 89 | 90 | render(); 91 | ``` 92 | 93 | 94 | 95 | ### Submit on enter 96 | 97 | When you're only looking for the final value when user presses enter, you can use `onSubmit` instead of `onChange` prop. 98 | 99 | ```tsx 100 | import React, {useState} from 'react'; 101 | import {render, Box, Text} from 'ink'; 102 | import {TextInput} from '@inkjs/ui'; 103 | 104 | function Example() { 105 | const [value, setValue] = useState(''); 106 | 107 | return ( 108 | 109 | 110 | Input value: "{value}" 111 | 112 | ); 113 | } 114 | 115 | render(); 116 | ``` 117 | 118 | 119 | 120 | ### Disabled 121 | 122 | When there are two or more text inputs, only one should be receiving user input at a time, while others should be disabled via `isDisabled` prop. 123 | 124 | ```tsx 125 | import React, {useState} from 'react'; 126 | import {render, Box, Text} from 'ink'; 127 | import {TextInput} from '@inkjs/ui'; 128 | 129 | function Example() { 130 | const [activeInput, setActiveInput] = useState('name'); 131 | const [name, setName] = useState(''); 132 | const [surname, setSurname] = useState(''); 133 | 134 | return ( 135 | 136 | 137 | { 142 | setActiveInput('surname'); 143 | }} 144 | /> 145 | 146 | { 151 | setActiveInput('none'); 152 | }} 153 | /> 154 | 155 | 156 | 157 | Name: "{name}" 158 | Surname: "{surname}" 159 | 160 | 161 | ); 162 | } 163 | 164 | render(); 165 | ``` 166 | 167 | 168 | 169 | ## Props 170 | 171 | ### isDisabled 172 | 173 | Type: `boolean`\ 174 | Default: `false` 175 | 176 | When disabled, user input is ignored. 177 | 178 | ### placeholder 179 | 180 | Type: `string` 181 | 182 | Text to display when input is empty. 183 | 184 | ### defaultValue 185 | 186 | Type: `string` 187 | 188 | Default input value. 189 | 190 | ### suggestions 191 | 192 | Type: `string[]` 193 | 194 | Suggestions to autocomplete the input value. 195 | 196 | ### onChange(value) 197 | 198 | Type: `Function` 199 | 200 | Callback when input value changes. 201 | 202 | #### value 203 | 204 | Type: `string` 205 | 206 | Input value. 207 | 208 | ### onSubmit(value) 209 | 210 | Type: `Function` 211 | 212 | Callback when enter is pressed. 213 | 214 | #### value 215 | 216 | Type: `string` 217 | 218 | Input value. 219 | -------------------------------------------------------------------------------- /docs/unordered-list.md: -------------------------------------------------------------------------------- 1 | # Unordered list 2 | 3 | > `UnorderedList` is used to show lists of items. 4 | 5 | [Theme](../source/components/unordered-list/theme.ts) | [Example code](../examples/unordered-list.tsx) 6 | 7 | ## Usage 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import {render, Box, Text} from 'ink'; 12 | import {UnorderedList} from '@inkjs/ui'; 13 | 14 | function Example() { 15 | return ( 16 | 17 | 18 | Red 19 | 20 | 21 | 22 | Green 23 | 24 | 25 | 26 | Light 27 | 28 | 29 | 30 | Dark 31 | 32 | 33 | 34 | 35 | 36 | Blue 37 | 38 | 39 | ); 40 | } 41 | 42 | render(); 43 | ``` 44 | 45 | 46 | 47 | ## Props 48 | 49 | ### UnorderedList 50 | 51 | #### children 52 | 53 | Type: `ReactNode` 54 | 55 | List items. 56 | 57 | ### UnorderedList.Item 58 | 59 | #### children 60 | 61 | Type: `ReactNode` 62 | 63 | List item content. 64 | -------------------------------------------------------------------------------- /examples/alert.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/alert.tsx 4 | */ 5 | 6 | import React from 'react'; 7 | import {render, Box} from 'ink'; 8 | import {Alert} from '../source/index.js'; 9 | 10 | function Example() { 11 | return ( 12 | 13 | A new version of this CLI is available 14 | 15 | Your license is expired 16 | 17 | 18 | Current version of this CLI has been deprecated 19 | 20 | 21 | API won't be available tomorrow night 22 | 23 | ); 24 | } 25 | 26 | render(); 27 | -------------------------------------------------------------------------------- /examples/autocomplete.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/autocomplete.tsx 4 | */ 5 | 6 | import React, {useMemo, useState} from 'react'; 7 | import {render, Box, Text} from 'ink'; 8 | import {TextInput, Select} from '../source/index.js'; 9 | 10 | function Example() { 11 | const [filterText, setFilterText] = useState(''); 12 | const [value, setValue] = useState(); 13 | 14 | const options = useMemo(() => { 15 | return [ 16 | { 17 | label: 'Red', 18 | value: 'red', 19 | }, 20 | { 21 | label: 'Green', 22 | value: 'green', 23 | }, 24 | { 25 | label: 'Yellow', 26 | value: 'yellow', 27 | }, 28 | { 29 | label: 'Blue', 30 | value: 'blue', 31 | }, 32 | { 33 | label: 'Magenta', 34 | value: 'magenta', 35 | }, 36 | { 37 | label: 'Cyan', 38 | value: 'cyan', 39 | }, 40 | { 41 | label: 'White', 42 | value: 'white', 43 | }, 44 | ].filter(option => option.label.includes(filterText)); 45 | }, [filterText]); 46 | 47 | return ( 48 | 49 | {!value && ( 50 | <> 51 | 52 | 53 | 48 | 49 | Selected value: {value} 50 | 51 | ); 52 | } 53 | 54 | render(); 55 | -------------------------------------------------------------------------------- /examples/spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/spinner.tsx 4 | */ 5 | 6 | import React from 'react'; 7 | import {render, Box} from 'ink'; 8 | import {Spinner} from '../source/index.js'; 9 | 10 | function Example() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | render(); 19 | -------------------------------------------------------------------------------- /examples/status-message.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/status-message.tsx 4 | */ 5 | 6 | import React from 'react'; 7 | import {render, Box} from 'ink'; 8 | import {StatusMessage} from '../source/index.js'; 9 | 10 | function Example() { 11 | return ( 12 | 13 | Success 14 | Error 15 | Warning 16 | Info 17 | 18 | ); 19 | } 20 | 21 | render(); 22 | -------------------------------------------------------------------------------- /examples/text-input.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/text-input.tsx 4 | */ 5 | 6 | import React, {useState} from 'react'; 7 | import {render, Box, Text} from 'ink'; 8 | import {TextInput} from '../source/index.js'; 9 | 10 | function Example() { 11 | const [value, setValue] = useState(''); 12 | 13 | return ( 14 | 15 | 16 | Input value: "{value}" 17 | 18 | ); 19 | } 20 | 21 | render(); 22 | -------------------------------------------------------------------------------- /examples/theming/custom-component.tsx: -------------------------------------------------------------------------------- 1 | import React, {render, Text, type TextProps} from 'ink'; 2 | import { 3 | ThemeProvider, 4 | defaultTheme, 5 | extendTheme, 6 | useComponentTheme, 7 | type ComponentTheme, 8 | } from '../../source/index.js'; 9 | 10 | const customLabelTheme = { 11 | styles: { 12 | label: (): TextProps => ({ 13 | color: 'green', 14 | }), 15 | }, 16 | } satisfies ComponentTheme; 17 | 18 | type CustomLabelTheme = typeof customLabelTheme; 19 | 20 | const customTheme = extendTheme(defaultTheme, { 21 | components: { 22 | CustomLabel: customLabelTheme, 23 | }, 24 | }); 25 | 26 | function CustomLabel() { 27 | const {styles} = useComponentTheme('CustomLabel'); 28 | 29 | return Hello world; 30 | } 31 | 32 | function Example() { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | render(); 41 | -------------------------------------------------------------------------------- /examples/theming/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box, type TextProps} from 'ink'; 3 | import { 4 | Spinner, 5 | ThemeProvider, 6 | defaultTheme, 7 | extendTheme, 8 | } from '../../source/index.js'; 9 | 10 | const customTheme = extendTheme(defaultTheme, { 11 | components: { 12 | Spinner: { 13 | styles: { 14 | frame: (): TextProps => ({ 15 | color: 'magenta', 16 | }), 17 | }, 18 | }, 19 | }, 20 | }); 21 | 22 | function Example() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | render(); 33 | -------------------------------------------------------------------------------- /examples/theming/unordered-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import { 4 | UnorderedList, 5 | ThemeProvider, 6 | extendTheme, 7 | defaultTheme, 8 | } from '../../source/index.js'; 9 | 10 | const customTheme = extendTheme(defaultTheme, { 11 | components: { 12 | UnorderedList: { 13 | config: () => ({ 14 | marker: '+', 15 | }), 16 | }, 17 | }, 18 | }); 19 | 20 | function Example() { 21 | return ( 22 | 23 | 24 | 25 | 26 | Red 27 | 28 | 29 | 30 | Green 31 | 32 | 33 | 34 | Light 35 | 36 | 37 | 38 | Dark 39 | 40 | 41 | 42 | 43 | 44 | Blue 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | render(); 53 | -------------------------------------------------------------------------------- /examples/unordered-list.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example: 3 | * npm run example examples/unordered-list.tsx 4 | */ 5 | 6 | import React from 'react'; 7 | import {render, Box, Text} from 'ink'; 8 | import {UnorderedList} from '../source/index.js'; 9 | 10 | function Example() { 11 | return ( 12 | 13 | 14 | 15 | Red 16 | 17 | 18 | 19 | Green 20 | 21 | 22 | 23 | Light 24 | 25 | 26 | 27 | Dark 28 | 29 | 30 | 31 | 32 | 33 | Blue 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | render(); 41 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vadym Demedes (github.com/vadimdemedes) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /media/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/alert.png -------------------------------------------------------------------------------- /media/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/badge.png -------------------------------------------------------------------------------- /media/confirm-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/confirm-input.png -------------------------------------------------------------------------------- /media/email-input-autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input-autocomplete.gif -------------------------------------------------------------------------------- /media/email-input-basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input-basic.gif -------------------------------------------------------------------------------- /media/email-input-default-value.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input-default-value.gif -------------------------------------------------------------------------------- /media/email-input-disabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input-disabled.gif -------------------------------------------------------------------------------- /media/email-input-submit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input-submit.gif -------------------------------------------------------------------------------- /media/email-input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/email-input.gif -------------------------------------------------------------------------------- /media/multi-select-basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/multi-select-basic.gif -------------------------------------------------------------------------------- /media/multi-select-default-value.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/multi-select-default-value.gif -------------------------------------------------------------------------------- /media/multi-select-disabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/multi-select-disabled.gif -------------------------------------------------------------------------------- /media/multi-select-submit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/multi-select-submit.gif -------------------------------------------------------------------------------- /media/multi-select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/multi-select.gif -------------------------------------------------------------------------------- /media/ordered-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/ordered-list.png -------------------------------------------------------------------------------- /media/password-input-basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/password-input-basic.gif -------------------------------------------------------------------------------- /media/password-input-disabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/password-input-disabled.gif -------------------------------------------------------------------------------- /media/password-input-submit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/password-input-submit.gif -------------------------------------------------------------------------------- /media/password-input.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/password-input.gif -------------------------------------------------------------------------------- /media/progress-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/progress-bar.gif -------------------------------------------------------------------------------- /media/select-basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/select-basic.gif -------------------------------------------------------------------------------- /media/select-default-value.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/select-default-value.gif -------------------------------------------------------------------------------- /media/select-disabled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/select-disabled.gif -------------------------------------------------------------------------------- /media/select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimdemedes/ink-ui/14b1145da0123a48cfc2f0ec9ff33dff0633f464/media/select.gif -------------------------------------------------------------------------------- /media/source/confirm-input/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import {ConfirmInput} from '../../../source/index.js'; 4 | 5 | function Example() { 6 | const [choice, setChoice] = useState<'agreed' | 'disagreed' | undefined>(); 7 | 8 | return ( 9 | 10 | {!choice && ( 11 | <> 12 | Do you agree with terms of service? 13 | { 15 | setChoice('agreed'); 16 | }} 17 | onCancel={() => { 18 | setChoice('disagreed'); 19 | }} 20 | /> 21 | 22 | )} 23 | 24 | {choice === 'agreed' && I know you haven't read them, but ok} 25 | {choice === 'disagreed' && Ok, whatever} 26 | 27 | ); 28 | } 29 | 30 | render(); 31 | -------------------------------------------------------------------------------- /media/source/confirm-input/readme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box} from 'ink'; 3 | import {ConfirmInput} from '../../../source/index.js'; 4 | 5 | function Demo() { 6 | return ( 7 | 8 | { 10 | // Confirmed 11 | }} 12 | onCancel={() => { 13 | // Cancelled 14 | }} 15 | /> 16 | 17 | ); 18 | } 19 | 20 | render(); 21 | -------------------------------------------------------------------------------- /media/source/email-input/autocomplete.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(''); 10 | 11 | return ( 12 | 13 | 18 | 19 | Input value: "{value}" 20 | 21 | ); 22 | } 23 | 24 | render(); 25 | 26 | await delay(500); 27 | await input('jane@'); 28 | await delay(500); 29 | await press('\r'); 30 | -------------------------------------------------------------------------------- /media/source/email-input/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(''); 10 | 11 | return ( 12 | 13 | 14 | Input value: "{value}" 15 | 16 | ); 17 | } 18 | 19 | render(); 20 | 21 | await delay(500); 22 | await input('jane@he'); 23 | await delay(250); 24 | await press('\r'); 25 | -------------------------------------------------------------------------------- /media/source/email-input/default-value.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../../../source/index.js'; 5 | import {del} from '../helpers/escapes.js'; 6 | import input from '../helpers/input.js'; 7 | import press from '../helpers/press.js'; 8 | 9 | function Example() { 10 | const [value, setValue] = useState('jane@hey.com'); 11 | 12 | return ( 13 | 14 | 19 | 20 | Input value: "{value}" 21 | 22 | ); 23 | } 24 | 25 | render(); 26 | 27 | await delay(500); 28 | await press(del, 12); 29 | await delay(250); 30 | await input('hopper@he'); 31 | await delay(250); 32 | await press('\r'); 33 | -------------------------------------------------------------------------------- /media/source/email-input/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [activeInput, setActiveInput] = useState('first'); 10 | const [first, setFirst] = useState(''); 11 | const [second, setSecond] = useState(''); 12 | 13 | return ( 14 | 15 | 16 | { 21 | setActiveInput('second'); 22 | }} 23 | /> 24 | 25 | { 30 | setActiveInput('none'); 31 | }} 32 | /> 33 | 34 | 35 | 36 | First email: "{first}" 37 | Second email: "{second}" 38 | 39 | 40 | ); 41 | } 42 | 43 | render(); 44 | 45 | await delay(500); 46 | await input('jane@he'); 47 | await delay(250); 48 | await press('\r'); 49 | await delay(250); 50 | await input('hopper@he'); 51 | await delay(250); 52 | await press('\r'); 53 | await delay(1000); 54 | -------------------------------------------------------------------------------- /media/source/email-input/readme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../source/index.js'; 5 | import {del} from '../helpers/escapes.js'; 6 | import input from '../helpers/input.js'; 7 | import press from '../helpers/press.js'; 8 | 9 | function Demo() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | render(); 18 | 19 | await delay(500); 20 | await input('jane@'); 21 | await delay(250); 22 | await input('ao'); 23 | await delay(250); 24 | await press(del, 2); 25 | await delay(250); 26 | await input('hey'); 27 | await delay(250); 28 | await press('\r'); 29 | -------------------------------------------------------------------------------- /media/source/email-input/submit.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {EmailInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(''); 10 | 11 | return ( 12 | 13 | 14 | Input value: "{value}" 15 | 16 | ); 17 | } 18 | 19 | render(); 20 | 21 | await delay(500); 22 | await input('jane@he'); 23 | await delay(250); 24 | await press('\r'); 25 | -------------------------------------------------------------------------------- /media/source/helpers/escapes.ts: -------------------------------------------------------------------------------- 1 | export const upArrow = '\u001B[A'; 2 | export const downArrow = '\u001B[B'; 3 | export const leftArrow = '\u001B[D'; 4 | export const rightArrow = '\u001B[C'; 5 | export const del = '\u007F'; 6 | -------------------------------------------------------------------------------- /media/source/helpers/input.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import delay from 'delay'; 3 | 4 | export default async function input(characters: string) { 5 | for (const character of characters) { 6 | process.stdin.emit('data', character); 7 | 8 | // eslint-disable-next-line no-await-in-loop 9 | await delay(200 * Math.random()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /media/source/helpers/press.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import delay from 'delay'; 3 | 4 | export default async function press(character: string, times = 1) { 5 | for (let index = 0; index < times; index++) { 6 | process.stdin.emit('data', character); 7 | 8 | // eslint-disable-next-line no-await-in-loop 9 | await delay(200 * Math.random()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /media/source/multi-select/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {MultiSelect} from '../../../source/index.js'; 5 | import {upArrow, downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState([]); 10 | 11 | return ( 12 | 13 | 46 | 47 | Selected values: {value.join(', ')} 48 | 49 | ); 50 | } 51 | 52 | render(); 53 | 54 | await delay(500); 55 | await press(downArrow); 56 | await delay(250); 57 | await press(' '); 58 | await delay(250); 59 | await press(downArrow, 5); 60 | await delay(250); 61 | await press(' '); 62 | await delay(250); 63 | await press(upArrow, 6); 64 | -------------------------------------------------------------------------------- /media/source/multi-select/default-value.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {MultiSelect} from '../../../source/index.js'; 5 | import {downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(['green']); 10 | 11 | return ( 12 | 13 | 47 | 48 | Selected values: {value.join(', ')} 49 | 50 | ); 51 | } 52 | 53 | render(); 54 | 55 | await delay(500); 56 | await press(downArrow, 2); 57 | await delay(250); 58 | await press(' '); 59 | await delay(250); 60 | await press(downArrow, 5); 61 | await delay(250); 62 | await press(' '); 63 | -------------------------------------------------------------------------------- /media/source/multi-select/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {MultiSelect} from '../../../source/index.js'; 5 | import {downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | const options = [ 9 | { 10 | label: 'Red', 11 | value: 'red', 12 | }, 13 | { 14 | label: 'Green', 15 | value: 'green', 16 | }, 17 | { 18 | label: 'Yellow', 19 | value: 'yellow', 20 | }, 21 | { 22 | label: 'Blue', 23 | value: 'blue', 24 | }, 25 | { 26 | label: 'Magenta', 27 | value: 'magenta', 28 | }, 29 | { 30 | label: 'Cyan', 31 | value: 'cyan', 32 | }, 33 | { 34 | label: 'White', 35 | value: 'white', 36 | }, 37 | ]; 38 | 39 | function Example() { 40 | const [activeInput, setActiveInput] = useState('primary'); 41 | const [primaryColors, setPrimaryColors] = useState([]); 42 | const [secondaryColors, setSecondaryColors] = useState([]); 43 | 44 | return ( 45 | 46 | 47 | { 52 | setActiveInput('secondary'); 53 | }} 54 | /> 55 | 56 | Primary colors: {primaryColors.join(', ')} 57 | 58 | 59 | 60 | { 65 | setActiveInput('none'); 66 | }} 67 | /> 68 | 69 | Secondary colors: {secondaryColors.join(', ')} 70 | 71 | 72 | ); 73 | } 74 | 75 | render(); 76 | 77 | await delay(500); 78 | await press(downArrow); 79 | await delay(250); 80 | await press(' '); 81 | await delay(250); 82 | await press(downArrow, 2); 83 | await delay(250); 84 | await press(' '); 85 | await delay(100); 86 | await press('\r'); 87 | await delay(250); 88 | await press(downArrow, 5); 89 | await delay(250); 90 | await press(' '); 91 | await delay(100); 92 | await press('\r'); 93 | await delay(1000); 94 | -------------------------------------------------------------------------------- /media/source/multi-select/readme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box} from 'ink'; 3 | import delay from 'delay'; 4 | import {MultiSelect} from '../../../source/index.js'; 5 | import {upArrow, downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | return ( 10 | 11 | 43 | 44 | ); 45 | } 46 | 47 | render(); 48 | 49 | await delay(500); 50 | await press(downArrow); 51 | await delay(250); 52 | await press(' '); 53 | await delay(250); 54 | await press(downArrow, 5); 55 | await delay(250); 56 | await press(' '); 57 | await delay(250); 58 | await press(upArrow, 6); 59 | -------------------------------------------------------------------------------- /media/source/multi-select/submit.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {MultiSelect} from '../../../source/index.js'; 5 | import {upArrow, downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState([]); 10 | 11 | return ( 12 | 13 | 46 | 47 | Selected values: {value.join(', ')} 48 | 49 | ); 50 | } 51 | 52 | render(); 53 | 54 | await delay(500); 55 | await press(downArrow); 56 | await delay(250); 57 | await press(' '); 58 | await delay(250); 59 | await press(downArrow, 5); 60 | await delay(250); 61 | await press(' '); 62 | await delay(250); 63 | await press('\r'); 64 | -------------------------------------------------------------------------------- /media/source/password-input/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {PasswordInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | 7 | function Example() { 8 | const [value, setValue] = useState(''); 9 | 10 | return ( 11 | 12 | 13 | Input value: "{value}" 14 | 15 | ); 16 | } 17 | 18 | render(); 19 | 20 | await delay(500); 21 | await input('secret'); 22 | -------------------------------------------------------------------------------- /media/source/password-input/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {PasswordInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [activeInput, setActiveInput] = useState('password'); 10 | const [password, setPassword] = useState(''); 11 | const [passwordConfirmation, setPasswordConfirmation] = useState(''); 12 | 13 | return ( 14 | 15 | 16 | { 21 | setActiveInput('passwordConfirmation'); 22 | }} 23 | /> 24 | 25 | { 30 | setActiveInput('none'); 31 | }} 32 | /> 33 | 34 | 35 | 36 | Password: "{password}" 37 | Password confirmation: "{passwordConfirmation}" 38 | 39 | 40 | ); 41 | } 42 | 43 | render(); 44 | 45 | await delay(500); 46 | await input('secret'); 47 | await delay(250); 48 | await press('\r'); 49 | await delay(250); 50 | await input('secret'); 51 | await delay(250); 52 | await press('\r'); 53 | await delay(1000); 54 | -------------------------------------------------------------------------------- /media/source/password-input/readme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box} from 'ink'; 3 | import delay from 'delay'; 4 | import {PasswordInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | 7 | function Demo() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | render(); 16 | 17 | await delay(500); 18 | await input('Hello world'); 19 | -------------------------------------------------------------------------------- /media/source/password-input/submit.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {PasswordInput} from '../../../source/index.js'; 5 | import input from '../helpers/input.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(''); 10 | 11 | return ( 12 | 13 | 14 | Input value: "{value}" 15 | 16 | ); 17 | } 18 | 19 | render(); 20 | 21 | await delay(500); 22 | await input('secret'); 23 | await delay(250); 24 | await press('\r'); 25 | -------------------------------------------------------------------------------- /media/source/select/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {Select} from '../../../source/index.js'; 5 | import {downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | const [value, setValue] = useState(); 10 | 11 | return ( 12 | 13 | 47 | 48 | Selected value: {value} 49 | 50 | ); 51 | } 52 | 53 | render(); 54 | 55 | await delay(500); 56 | await press(downArrow, 2); 57 | await delay(250); 58 | await press('\r'); 59 | await delay(250); 60 | await press(downArrow, 5); 61 | await delay(250); 62 | await press('\r'); 63 | -------------------------------------------------------------------------------- /media/source/select/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {render, Box, Text} from 'ink'; 3 | import delay from 'delay'; 4 | import {Select} from '../../../source/index.js'; 5 | import {downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | const options = [ 9 | { 10 | label: 'Red', 11 | value: 'red', 12 | }, 13 | { 14 | label: 'Green', 15 | value: 'green', 16 | }, 17 | { 18 | label: 'Yellow', 19 | value: 'yellow', 20 | }, 21 | { 22 | label: 'Blue', 23 | value: 'blue', 24 | }, 25 | { 26 | label: 'Magenta', 27 | value: 'magenta', 28 | }, 29 | { 30 | label: 'Cyan', 31 | value: 'cyan', 32 | }, 33 | { 34 | label: 'White', 35 | value: 'white', 36 | }, 37 | ]; 38 | 39 | function Example() { 40 | const [activeInput, setActiveInput] = useState('primary'); 41 | const [primaryColor, setPrimaryColor] = useState(); 42 | const [secondaryColor, setSecondaryColor] = useState(); 43 | 44 | return ( 45 | 46 | 47 | { 64 | setSecondaryColor(value); 65 | setActiveInput('none'); 66 | }} 67 | /> 68 | 69 | Secondary color: {secondaryColor} 70 | 71 | 72 | ); 73 | } 74 | 75 | render(); 76 | 77 | await delay(500); 78 | await press(downArrow); 79 | await delay(250); 80 | await press('\r'); 81 | await delay(250); 82 | await press(downArrow, 5); 83 | await delay(250); 84 | await press('\r'); 85 | await delay(1000); 86 | -------------------------------------------------------------------------------- /media/source/select/readme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, Box} from 'ink'; 3 | import delay from 'delay'; 4 | import {Select} from '../../../source/index.js'; 5 | import {upArrow, downArrow} from '../helpers/escapes.js'; 6 | import press from '../helpers/press.js'; 7 | 8 | function Example() { 9 | return ( 10 | 11 | , 48 | ); 49 | 50 | t.is( 51 | lastFrame(), 52 | [ 53 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')}`, 54 | ' Green', 55 | ' Yellow', 56 | ' Blue', 57 | ' Magenta', 58 | ' Cyan', 59 | ].join('\n'), 60 | ); 61 | }); 62 | 63 | test('focus next option', async t => { 64 | const {lastFrame, stdin} = render(); 84 | 85 | for (let press = 0; press < 6; press++) { 86 | await delay(50); 87 | stdin.write(arrowDown); 88 | await delay(50); 89 | } 90 | 91 | t.is( 92 | lastFrame(), 93 | [ 94 | ' Yellow', 95 | ' Blue', 96 | ' Magenta', 97 | ' Cyan', 98 | `${chalk.blue(figures.pointer)} ${chalk.blue('White')}`, 99 | ].join('\n'), 100 | ); 101 | }); 102 | 103 | test("don't scroll down when focused option is the last one", async t => { 104 | const {lastFrame, stdin} = render(); 126 | 127 | await delay(50); 128 | stdin.write(arrowDown); 129 | await delay(50); 130 | 131 | t.is( 132 | lastFrame(), 133 | [ 134 | ' Red', 135 | `${chalk.blue(figures.pointer)} ${chalk.blue('Green')}`, 136 | ' Yellow', 137 | ' Blue', 138 | ' Magenta', 139 | ].join('\n'), 140 | ); 141 | 142 | await delay(50); 143 | stdin.write(arrowUp); 144 | await delay(50); 145 | 146 | t.is( 147 | lastFrame(), 148 | [ 149 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')}`, 150 | ' Green', 151 | ' Yellow', 152 | ' Blue', 153 | ' Magenta', 154 | ].join('\n'), 155 | ); 156 | }); 157 | 158 | test('focus previous option and scroll up', async t => { 159 | const {lastFrame, stdin} = render(); 198 | 199 | for (let press = 0; press < 6; press++) { 200 | await delay(50); 201 | stdin.write(arrowDown); 202 | await delay(50); 203 | } 204 | 205 | t.is( 206 | lastFrame(), 207 | [ 208 | ' Yellow', 209 | ' Blue', 210 | ' Magenta', 211 | ' Cyan', 212 | `${chalk.blue(figures.pointer)} ${chalk.blue('White')}`, 213 | ].join('\n'), 214 | ); 215 | 216 | for (let press = 0; press < 7; press++) { 217 | await delay(50); 218 | stdin.write(arrowUp); 219 | await delay(50); 220 | } 221 | 222 | t.is( 223 | lastFrame(), 224 | [ 225 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')}`, 226 | ' Green', 227 | ' Yellow', 228 | ' Blue', 229 | ' Magenta', 230 | ].join('\n'), 231 | ); 232 | }); 233 | 234 | test('ignore input when disabled', async t => { 235 | let value: string | undefined; 236 | 237 | const {lastFrame, stdin} = render( 238 | { 295 | value = newValue; 296 | }} 297 | />, 298 | ); 299 | 300 | t.is( 301 | lastFrame(), 302 | [ 303 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')}`, 304 | ' Green', 305 | ' Yellow', 306 | ' Blue', 307 | ' Magenta', 308 | ].join('\n'), 309 | ); 310 | 311 | t.is(value, undefined); 312 | 313 | await delay(50); 314 | stdin.write(enter); 315 | await delay(50); 316 | 317 | t.is( 318 | lastFrame(), 319 | [ 320 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')} ${chalk.green( 321 | figures.tick, 322 | )}`, 323 | ' Green', 324 | ' Yellow', 325 | ' Blue', 326 | ' Magenta', 327 | ].join('\n'), 328 | ); 329 | 330 | t.is(value, 'red'); 331 | 332 | await delay(50); 333 | stdin.write(arrowDown); 334 | await delay(50); 335 | 336 | t.is( 337 | lastFrame(), 338 | [ 339 | ` ${chalk.green('Red')} ${chalk.green(figures.tick)}`, 340 | `${chalk.blue(figures.pointer)} ${chalk.blue('Green')}`, 341 | ' Yellow', 342 | ' Blue', 343 | ' Magenta', 344 | ].join('\n'), 345 | ); 346 | }); 347 | 348 | test('selected option by default', t => { 349 | const {lastFrame} = render(); 365 | 366 | t.is( 367 | lastFrame(), 368 | [ 369 | `${chalk.blue(figures.pointer)} ${chalk.blue('Red')}`, 370 | ' Green', 371 | ` Ye${chalk.bold('l')}low`, 372 | ` B${chalk.bold('l')}ue`, 373 | ' Magenta', 374 | ].join('\n'), 375 | ); 376 | }); 377 | -------------------------------------------------------------------------------- /test/spinner.tsx: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import React from 'react'; 3 | import test from 'ava'; 4 | import {render} from 'ink-testing-library'; 5 | import chalk from 'chalk'; 6 | import spinners from 'cli-spinners'; 7 | import delay from 'delay'; 8 | import {Spinner} from '../source/index.js'; 9 | 10 | test('spinner', async t => { 11 | const {frames, unmount} = render(); 12 | 13 | const spinner = spinners.dots; 14 | await delay(spinner.frames.length * spinner.interval); 15 | unmount(); 16 | 17 | const uniqueFrames = [...new Set(frames)]; 18 | 19 | if (process.env['CI'] && uniqueFrames.at(-1) === '\n') { 20 | uniqueFrames.pop(); 21 | } 22 | 23 | t.deepEqual( 24 | uniqueFrames, 25 | spinner.frames.map(frame => `${chalk.blue(frame)} Loading`), 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /test/status-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import test from 'ava'; 3 | import {Box} from 'ink'; 4 | import {render} from 'ink-testing-library'; 5 | import chalk from 'chalk'; 6 | import figures from 'figures'; 7 | import {StatusMessage} from '../source/index.js'; 8 | 9 | test.failing('success', t => { 10 | const {lastFrame} = render( 11 | Success, 12 | ); 13 | 14 | t.is(lastFrame(), `${chalk.green(figures.tick)} Success`); 15 | }); 16 | 17 | test('error', t => { 18 | const {lastFrame} = render( 19 | Error, 20 | ); 21 | 22 | t.is(lastFrame(), `${chalk.red(figures.cross)} Error`); 23 | }); 24 | 25 | test.failing('warning', t => { 26 | const {lastFrame} = render( 27 | Warning, 28 | ); 29 | 30 | t.is(lastFrame(), `${chalk.yellow(figures.warning)} Warning`); 31 | }); 32 | 33 | test.failing('info', t => { 34 | const {lastFrame} = render( 35 | Info, 36 | ); 37 | 38 | t.is(lastFrame(), `${chalk.blue(figures.info)} Info`); 39 | }); 40 | 41 | test.failing('multiline message', t => { 42 | const {lastFrame} = render( 43 | 44 | Hello world 45 | , 46 | ); 47 | 48 | t.is( 49 | lastFrame(), 50 | [`${chalk.blue(figures.info)} Hello`, ' world'].join('\n'), 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /test/unordered-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import test from 'ava'; 3 | import {Text} from 'ink'; 4 | import {render} from 'ink-testing-library'; 5 | import chalk from 'chalk'; 6 | import { 7 | ThemeProvider, 8 | UnorderedList, 9 | defaultTheme, 10 | extendTheme, 11 | } from '../source/index.js'; 12 | 13 | test('unordered list', t => { 14 | const {lastFrame} = render( 15 | 16 | 17 | Red 18 | 19 | 20 | Green 21 | 22 | 23 | Yellow 24 | 25 | , 26 | ); 27 | 28 | t.is( 29 | lastFrame(), 30 | [ 31 | `${chalk.dim('─')} Red`, 32 | `${chalk.dim('─')} Green`, 33 | `${chalk.dim('─')} Yellow`, 34 | ].join('\n'), 35 | ); 36 | }); 37 | 38 | test('custom marker', t => { 39 | const customTheme = extendTheme(defaultTheme, { 40 | components: { 41 | UnorderedList: { 42 | config: () => ({ 43 | marker: '+', 44 | }), 45 | }, 46 | }, 47 | }); 48 | 49 | const {lastFrame} = render( 50 | 51 | 52 | 53 | Red 54 | 55 | 56 | Green 57 | 58 | 59 | Yellow 60 | 61 | 62 | , 63 | ); 64 | 65 | t.is( 66 | lastFrame(), 67 | [ 68 | `${chalk.dim('+')} Red`, 69 | `${chalk.dim('+')} Green`, 70 | `${chalk.dim('+')} Yellow`, 71 | ].join('\n'), 72 | ); 73 | }); 74 | 75 | test('nested list', t => { 76 | const {lastFrame} = render( 77 | 78 | 79 | Red 80 | 81 | 82 | Green 83 | 84 | 85 | 86 | Light 87 | 88 | 89 | 90 | Normal 91 | 92 | 93 | 94 | Dark 95 | 96 | 97 | 98 | 99 | Yellow 100 | 101 | , 102 | ); 103 | 104 | t.is( 105 | lastFrame(), 106 | [ 107 | `${chalk.dim('─')} Red`, 108 | `${chalk.dim('─')} Green`, 109 | [ 110 | `${chalk.dim('─')} Light`, 111 | `${chalk.dim('─')} Normal`, 112 | `${chalk.dim('─')} Dark`, 113 | ] 114 | .map(line => ` ${line}`) 115 | .join('\n'), 116 | `${chalk.dim('─')} Yellow`, 117 | ].join('\n'), 118 | ); 119 | }); 120 | 121 | test('custom marker in nested list', t => { 122 | const customTheme = extendTheme(defaultTheme, { 123 | components: { 124 | UnorderedList: { 125 | config: () => ({ 126 | marker: '+', 127 | }), 128 | }, 129 | }, 130 | }); 131 | 132 | const {lastFrame} = render( 133 | 134 | 135 | 136 | Red 137 | 138 | 139 | Green 140 | 141 | 142 | 143 | Light 144 | 145 | 146 | 147 | Normal 148 | 149 | 150 | 151 | Dark 152 | 153 | 154 | 155 | 156 | Yellow 157 | 158 | 159 | , 160 | ); 161 | 162 | t.is( 163 | lastFrame(), 164 | [ 165 | `${chalk.dim('+')} Red`, 166 | `${chalk.dim('+')} Green`, 167 | [ 168 | `${chalk.dim('+')} Light`, 169 | `${chalk.dim('+')} Normal`, 170 | `${chalk.dim('+')} Dark`, 171 | ] 172 | .map(line => ` ${line}`) 173 | .join('\n'), 174 | `${chalk.dim('+')} Yellow`, 175 | ].join('\n'), 176 | ); 177 | }); 178 | 179 | test('custom markers in nested lists', t => { 180 | const customTheme = extendTheme(defaultTheme, { 181 | components: { 182 | UnorderedList: { 183 | config: () => ({ 184 | marker: ['─', '+'], 185 | }), 186 | }, 187 | }, 188 | }); 189 | 190 | const {lastFrame} = render( 191 | 192 | 193 | 194 | Red 195 | 196 | 197 | Green 198 | 199 | 200 | 201 | Light 202 | 203 | 204 | 205 | Normal 206 | 207 | 208 | 209 | Dark 210 | 211 | 212 | 213 | 214 | Yellow 215 | 216 | 217 | , 218 | ); 219 | 220 | t.is( 221 | lastFrame(), 222 | [ 223 | `${chalk.dim('─')} Red`, 224 | `${chalk.dim('─')} Green`, 225 | [ 226 | `${chalk.dim('+')} Light`, 227 | `${chalk.dim('+')} Normal`, 228 | `${chalk.dim('+')} Dark`, 229 | ] 230 | .map(line => ` ${line}`) 231 | .join('\n'), 232 | `${chalk.dim('─')} Yellow`, 233 | ].join('\n'), 234 | ); 235 | }); 236 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "sourceMap": true, 6 | "jsx": "react" 7 | }, 8 | "include": [ 9 | "source" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------