diff --git a/.changeset/early-deers-cough.md b/.changeset/early-deers-cough.md new file mode 100644 index 00000000..9959837f --- /dev/null +++ b/.changeset/early-deers-cough.md @@ -0,0 +1,6 @@ +--- +'@clack/prompts': minor +'@clack/core': minor +--- + +feat: added `selectableGroups` option for inactive group headers for `GroupMultiSelectPrompt` diff --git a/packages/core/src/prompts/group-multiselect.ts b/packages/core/src/prompts/group-multiselect.ts index b5440539..c1adb618 100644 --- a/packages/core/src/prompts/group-multiselect.ts +++ b/packages/core/src/prompts/group-multiselect.ts @@ -6,10 +6,12 @@ interface GroupMultiSelectOptions initialValues?: T['value'][]; required?: boolean; cursorAt?: T['value']; + selectableGroups?: boolean; } export default class GroupMultiSelectPrompt extends Prompt { options: (T & { group: string | boolean })[]; cursor: number = 0; + #selectableGroups: boolean; getGroupItems(group: string): T[] { return this.options.filter((o) => o.group === group); @@ -17,7 +19,7 @@ export default class GroupMultiSelectPrompt extends Pr isGroupSelected(group: string) { const items = this.getGroupItems(group); - return items.every((i) => this.value.includes(i.value)); + return this.#selectableGroups && items.every((i) => this.value.includes(i.value)); } private toggleValue() { @@ -44,6 +46,7 @@ export default class GroupMultiSelectPrompt extends Pr constructor(opts: GroupMultiSelectOptions) { super(opts, false); const { options } = opts; + this.#selectableGroups = opts.selectableGroups ?? true; this.options = Object.entries(options).flatMap(([key, option]) => [ { value: key, group: true, label: key }, ...option.map((opt) => ({ ...opt, group: key })), @@ -51,7 +54,7 @@ export default class GroupMultiSelectPrompt extends Pr this.value = [...(opts.initialValues ?? [])]; this.cursor = Math.max( this.options.findIndex(({ value }) => value === opts.cursorAt), - 0 + this.#selectableGroups ? 0 : 1 ); this.on('cursor', (key) => { @@ -59,10 +62,16 @@ export default class GroupMultiSelectPrompt extends Pr case 'left': case 'up': this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1; + if (!this.#selectableGroups && this.options[this.cursor].group === true) { + this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1; + } break; case 'down': case 'right': this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1; + if (!this.#selectableGroups && this.options[this.cursor].group === true) { + this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1; + } break; case 'space': this.toggleValue(); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index b6071bb5..b7d33c3c 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -428,8 +428,11 @@ export interface GroupMultiSelectOptions { initialValues?: Value[]; required?: boolean; cursorAt?: Value; + selectableGroups?: boolean; + spacedGroups?: boolean; } export const groupMultiselect = (opts: GroupMultiSelectOptions) => { + const { selectableGroups = false, spacedGroups = false } = opts; const opt = ( option: Option, state: @@ -447,28 +450,33 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const isItem = typeof (option as any).group === 'string'; const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); const isLast = isItem && (next as any).group === true; - const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''; + const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; + const spacingPrefix = spacedGroups && !isItem ? `\n${color.cyan(S_BAR)} ` : ''; if (state === 'active') { - return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ + return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } else if (state === 'group-active') { - return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; } else if (state === 'group-active-selected') { - return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } else if (state === 'selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim( + label + )}`; } else if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; + return `${spacingPrefix}${color.strikethrough(color.dim(label))}`; } else if (state === 'active-selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ + return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } else if (state === 'submitted') { return `${color.dim(label)}`; } - return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${spacingPrefix}${color.dim(prefix)}${ + isItem || selectableGroups ? `${color.dim(S_CHECKBOX_INACTIVE)} ` : '' + }${color.dim(label)}`; }; return new GroupMultiSelectPrompt({ @@ -476,6 +484,7 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => initialValues: opts.initialValues, required: opts.required ?? true, cursorAt: opts.cursorAt, + selectableGroups: selectableGroups, validate(selected: Value[]) { if (this.required && selected.length === 0) return `Please select at least one option.\n${color.reset(