[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

feat(@clack/promtps): multiselect maxItems

Bruno Rocha (Aug 26, 2023, 12:55 PM -0300) b5c6b9b4 24a3f4a2

+81 -69
+5
.changeset/bright-rules-yell.md
··· 1 + --- 2 + '@clack/prompts': patch 3 + --- 4 + 5 + Feat multiselect maxItems option
+76 -69
packages/prompts/src/index.ts
··· 58 58 } 59 59 }; 60 60 61 + interface LimitOptionsParams<TOption> { 62 + options: TOption[]; 63 + maxItems: number | undefined; 64 + cursor: number; 65 + style: (option: TOption, active: boolean) => string; 66 + } 67 + 68 + const limitOptions = <TOption>(params: LimitOptionsParams<TOption>): string[] => { 69 + const { cursor, options, style } = params; 70 + 71 + // We clamp to minimum 5 because anything less doesn't make sense UX wise 72 + const maxItems = params.maxItems === undefined ? Infinity : Math.max(params.maxItems, 5); 73 + let slidingWindowLocation = 0; 74 + 75 + if (cursor >= slidingWindowLocation + maxItems - 3) { 76 + slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); 77 + } else if (cursor < slidingWindowLocation + 2) { 78 + slidingWindowLocation = Math.max(cursor - 2, 0); 79 + } 80 + 81 + const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; 82 + const shouldRenderBottomEllipsis = 83 + maxItems < options.length && slidingWindowLocation + maxItems < options.length; 84 + 85 + return options 86 + .slice(slidingWindowLocation, slidingWindowLocation + maxItems) 87 + .map((option, i, arr) => { 88 + const isTopLimit = i === 0 && shouldRenderTopEllipsis; 89 + const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; 90 + return isTopLimit || isBottomLimit 91 + ? color.dim('...') 92 + : style(option, i + slidingWindowLocation === cursor); 93 + }); 94 + }; 95 + 61 96 export interface TextOptions { 62 97 message: string; 63 98 placeholder?: string; ··· 184 219 export const select = <Value>(opts: SelectOptions<Value>) => { 185 220 const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { 186 221 const label = option.label ?? String(option.value); 187 - if (state === 'active') { 188 - return `${color.green(S_RADIO_ACTIVE)} ${label} ${ 189 - option.hint ? color.dim(`(${option.hint})`) : '' 190 - }`; 191 - } else if (state === 'selected') { 192 - return `${color.dim(label)}`; 193 - } else if (state === 'cancelled') { 194 - return `${color.strikethrough(color.dim(label))}`; 222 + switch (state) { 223 + case 'selected': 224 + return `${color.dim(label)}`; 225 + case 'active': 226 + return `${color.green(S_RADIO_ACTIVE)} ${label} ${ 227 + option.hint ? color.dim(`(${option.hint})`) : '' 228 + }`; 229 + case 'cancelled': 230 + return `${color.strikethrough(color.dim(label))}`; 231 + default: 232 + return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 195 233 } 196 - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 197 234 }; 198 - 199 - let slidingWindowLocation = 0; 200 235 201 236 return new SelectPrompt({ 202 237 options: opts.options, ··· 213 248 'cancelled' 214 249 )}\n${color.gray(S_BAR)}`; 215 250 default: { 216 - // We clamp to minimum 5 because anything less doesn't make sense UX wise 217 - const maxItems = opts.maxItems === undefined ? Infinity : Math.max(opts.maxItems, 5); 218 - if (this.cursor >= slidingWindowLocation + maxItems - 3) { 219 - slidingWindowLocation = Math.max( 220 - Math.min(this.cursor - maxItems + 3, this.options.length - maxItems), 221 - 0 222 - ); 223 - } else if (this.cursor < slidingWindowLocation + 2) { 224 - slidingWindowLocation = Math.max(this.cursor - 2, 0); 225 - } 226 - 227 - const shouldRenderTopEllipsis = 228 - maxItems < this.options.length && slidingWindowLocation > 0; 229 - const shouldRenderBottomEllipsis = 230 - maxItems < this.options.length && 231 - slidingWindowLocation + maxItems < this.options.length; 232 - 233 - return `${title}${color.cyan(S_BAR)} ${this.options 234 - .slice(slidingWindowLocation, slidingWindowLocation + maxItems) 235 - .map((option, i, arr) => { 236 - if (i === 0 && shouldRenderTopEllipsis) { 237 - return color.dim('...'); 238 - } else if (i === arr.length - 1 && shouldRenderBottomEllipsis) { 239 - return color.dim('...'); 240 - } else { 241 - return opt( 242 - option, 243 - i + slidingWindowLocation === this.cursor ? 'active' : 'inactive' 244 - ); 245 - } 246 - }) 247 - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 251 + return `${title}${color.cyan(S_BAR)} ${limitOptions({ 252 + cursor: this.cursor, 253 + options: this.options, 254 + maxItems: opts.maxItems, 255 + style: (item, active) => opt(item, active ? 'active' : 'inactive'), 256 + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 248 257 } 249 258 } 250 259 }, ··· 301 310 message: string; 302 311 options: Option<Value>[]; 303 312 initialValues?: Value[]; 313 + maxItems?: number; 304 314 required?: boolean; 305 315 cursorAt?: Value; 306 316 } ··· 346 356 render() { 347 357 let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 348 358 359 + const styleOption = (option: Option<Value>, active: boolean) => { 360 + const selected = this.value.includes(option.value); 361 + if (active && selected) { 362 + return opt(option, 'active-selected'); 363 + } 364 + if (selected) { 365 + return opt(option, 'selected'); 366 + } 367 + return opt(option, active ? 'active' : 'inactive'); 368 + }; 369 + 349 370 switch (this.state) { 350 371 case 'submit': { 351 372 return `${title}${color.gray(S_BAR)} ${ ··· 375 396 title + 376 397 color.yellow(S_BAR) + 377 398 ' ' + 378 - this.options 379 - .map((option, i) => { 380 - const selected = this.value.includes(option.value); 381 - const active = i === this.cursor; 382 - if (active && selected) { 383 - return opt(option, 'active-selected'); 384 - } 385 - if (selected) { 386 - return opt(option, 'selected'); 387 - } 388 - return opt(option, active ? 'active' : 'inactive'); 389 - }) 390 - .join(`\n${color.yellow(S_BAR)} `) + 399 + limitOptions({ 400 + options: this.options, 401 + cursor: this.cursor, 402 + maxItems: opts.maxItems, 403 + style: styleOption, 404 + }).join(`\n${color.yellow(S_BAR)} `) + 391 405 '\n' + 392 406 footer + 393 407 '\n' 394 408 ); 395 409 } 396 410 default: { 397 - return `${title}${color.cyan(S_BAR)} ${this.options 398 - .map((option, i) => { 399 - const selected = this.value.includes(option.value); 400 - const active = i === this.cursor; 401 - if (active && selected) { 402 - return opt(option, 'active-selected'); 403 - } 404 - if (selected) { 405 - return opt(option, 'selected'); 406 - } 407 - return opt(option, active ? 'active' : 'inactive'); 408 - }) 409 - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 411 + return `${title}${color.cyan(S_BAR)} ${limitOptions({ 412 + options: this.options, 413 + cursor: this.cursor, 414 + maxItems: opts.maxItems, 415 + style: styleOption, 416 + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 410 417 } 411 418 } 412 419 },