@@ -9,12 +9,14 @@ import {
99 S_CHECKBOX_SELECTED ,
1010 symbol ,
1111} from './common.js' ;
12+ import { limitOptions } from './limit-options.js' ;
1213import type { Option } from './select.js' ;
1314
1415export interface GroupMultiSelectOptions < Value > extends CommonOptions {
1516 message : string ;
1617 options : Record < string , Option < Value > [ ] > ;
1718 initialValues ?: Value [ ] ;
19+ maxItems ?: number ;
1820 required ?: boolean ;
1921 cursorAt ?: Value ;
2022 selectableGroups ?: boolean ;
@@ -42,8 +44,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
4244 const prefix = isItem ? ( selectableGroups ? `${ isLast ? S_BAR_END : S_BAR } ` : ' ' ) : '' ;
4345 let spacingPrefix = '' ;
4446 if ( groupSpacing > 0 && ! isItem ) {
45- const spacingPrefixText = `\n${ styleText ( 'cyan' , S_BAR ) } ` ;
46- spacingPrefix = `${ spacingPrefixText . repeat ( groupSpacing - 1 ) } ${ spacingPrefixText } ` ;
47+ spacingPrefix = '\n' . repeat ( groupSpacing ) ;
4748 }
4849
4950 if ( state === 'active' ) {
@@ -108,6 +109,30 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
108109 const title = `${ hasGuide ? `${ styleText ( 'gray' , S_BAR ) } \n` : '' } ${ symbol ( this . state ) } ${ opts . message } \n` ;
109110 const value = this . value ?? [ ] ;
110111
112+ const styleOption = (
113+ option : Option < Value > & { group : string | boolean } ,
114+ active : boolean
115+ ) => {
116+ const options = this . options ;
117+ const selected =
118+ value . includes ( option . value ) ||
119+ ( option . group === true && this . isGroupSelected ( `${ option . value } ` ) ) ;
120+ const groupActive =
121+ ! active &&
122+ typeof option . group === 'string' &&
123+ this . options [ this . cursor ] . value === option . group ;
124+ if ( groupActive ) {
125+ return opt ( option , selected ? 'group-active-selected' : 'group-active' , options ) ;
126+ }
127+ if ( active && selected ) {
128+ return opt ( option , 'active-selected' , options ) ;
129+ }
130+ if ( selected ) {
131+ return opt ( option , 'selected' , options ) ;
132+ }
133+ return opt ( option , active ? 'active' : 'inactive' , options ) ;
134+ } ;
135+
111136 switch ( this . state ) {
112137 case 'submit' : {
113138 const selectedOptions = this . options
@@ -127,6 +152,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
127152 } `;
128153 }
129154 case 'error' : {
155+ const guidePrefix = hasGuide ? `${ styleText ( 'yellow' , S_BAR ) } ` : '' ;
130156 const footer = this . error
131157 . split ( '\n' )
132158 . map ( ( ln , i ) =>
@@ -135,60 +161,35 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
135161 : ` ${ ln } `
136162 )
137163 . join ( '\n' ) ;
138- return `${ title } ${ hasGuide ? `${ styleText ( 'yellow' , S_BAR ) } ` : '' } ${ this . options
139- . map ( ( option , i , options ) => {
140- const selected =
141- value . includes ( option . value ) ||
142- ( option . group === true && this . isGroupSelected ( `${ option . value } ` ) ) ;
143- const active = i === this . cursor ;
144- const groupActive =
145- ! active &&
146- typeof option . group === 'string' &&
147- this . options [ this . cursor ] . value === option . group ;
148- if ( groupActive ) {
149- return opt ( option , selected ? 'group-active-selected' : 'group-active' , options ) ;
150- }
151- if ( active && selected ) {
152- return opt ( option , 'active-selected' , options ) ;
153- }
154- if ( selected ) {
155- return opt ( option , 'selected' , options ) ;
156- }
157- return opt ( option , active ? 'active' : 'inactive' , options ) ;
158- } )
159- . join ( `\n${ hasGuide ? `${ styleText ( 'yellow' , S_BAR ) } ` : '' } ` ) } \n${ footer } \n`;
164+ // Calculate rowPadding: title lines + footer lines (error message + trailing newline)
165+ const titleLineCount = title . split ( '\n' ) . length ;
166+ const footerLineCount = footer . split ( '\n' ) . length + 1 ; // footer + trailing newline
167+ const optionsText = limitOptions ( {
168+ output : opts . output ,
169+ options : this . options ,
170+ cursor : this . cursor ,
171+ maxItems : opts . maxItems ,
172+ columnPadding : guidePrefix . length ,
173+ rowPadding : titleLineCount + footerLineCount ,
174+ style : styleOption ,
175+ } ) . join ( `\n${ guidePrefix } ` ) ;
176+ return `${ title } ${ guidePrefix } ${ optionsText } \n${ footer } \n` ;
160177 }
161178 default : {
162- const optionsText = this . options
163- . map ( ( option , i , options ) => {
164- const selected =
165- value . includes ( option . value ) ||
166- ( option . group === true && this . isGroupSelected ( `${ option . value } ` ) ) ;
167- const active = i === this . cursor ;
168- const groupActive =
169- ! active &&
170- typeof option . group === 'string' &&
171- this . options [ this . cursor ] . value === option . group ;
172- let optionText = '' ;
173- if ( groupActive ) {
174- optionText = opt (
175- option ,
176- selected ? 'group-active-selected' : 'group-active' ,
177- options
178- ) ;
179- } else if ( active && selected ) {
180- optionText = opt ( option , 'active-selected' , options ) ;
181- } else if ( selected ) {
182- optionText = opt ( option , 'selected' , options ) ;
183- } else {
184- optionText = opt ( option , active ? 'active' : 'inactive' , options ) ;
185- }
186- const prefix = i !== 0 && ! optionText . startsWith ( '\n' ) ? ' ' : '' ;
187- return `${ prefix } ${ optionText } ` ;
188- } )
189- . join ( `\n${ hasGuide ? styleText ( 'cyan' , S_BAR ) : '' } ` ) ;
190- const optionsPrefix = optionsText . startsWith ( '\n' ) ? '' : ' ' ;
191- return `${ title } ${ hasGuide ? styleText ( 'cyan' , S_BAR ) : '' } ${ optionsPrefix } ${ optionsText } \n${
179+ const guidePrefix = hasGuide ? `${ styleText ( 'cyan' , S_BAR ) } ` : '' ;
180+ // Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
181+ const titleLineCount = title . split ( '\n' ) . length ;
182+ const footerLineCount = ( hasGuide ? 1 : 0 ) + 1 ; // guide line + trailing newline
183+ const optionsText = limitOptions ( {
184+ output : opts . output ,
185+ options : this . options ,
186+ cursor : this . cursor ,
187+ maxItems : opts . maxItems ,
188+ columnPadding : guidePrefix . length ,
189+ rowPadding : titleLineCount + footerLineCount ,
190+ style : styleOption ,
191+ } ) . join ( `\n${ guidePrefix } ` ) ;
192+ return `${ title } ${ guidePrefix } ${ optionsText } \n${
192193 hasGuide ? styleText ( 'cyan' , S_BAR_END ) : ''
193194 } \n`;
194195 }
0 commit comments