Skip to content

Commit 5522fdb

Browse files
committed
refactor(core + prompts): replace picocolors with styleText utility for consistent styling across prompts
1 parent aea4573 commit 5522fdb

28 files changed

+285
-248
lines changed

packages/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"test": "vitest run"
5454
},
5555
"dependencies": {
56-
"picocolors": "^1.0.0",
5756
"sisteransi": "^1.0.5"
5857
},
5958
"devDependencies": {

packages/core/src/prompts/autocomplete.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Key } from 'node:readline';
2-
import color from 'picocolors';
2+
import { styleText } from 'node:util';
33
import Prompt, { type PromptOptions } from './prompt.js';
44

55
interface OptionLike {
@@ -71,14 +71,14 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
7171

7272
get userInputWithCursor() {
7373
if (!this.userInput) {
74-
return color.inverse(color.hidden('_'));
74+
return styleText('inverse', styleText('hidden', '_'));
7575
}
7676
if (this._cursor >= this.userInput.length) {
7777
return `${this.userInput}█`;
7878
}
7979
const s1 = this.userInput.slice(0, this._cursor);
8080
const [s2, ...s3] = this.userInput.slice(this._cursor);
81-
return `${s1}${color.inverse(s2)}${s3.join('')}`;
81+
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
8282
}
8383

8484
get options(): T[] {

packages/core/src/prompts/password.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import Prompt, { type PromptOptions } from './prompt.js';
33

44
interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
@@ -18,12 +18,12 @@ export default class PasswordPrompt extends Prompt<string> {
1818
}
1919
const userInput = this.userInput;
2020
if (this.cursor >= userInput.length) {
21-
return `${this.masked}${color.inverse(color.hidden('_'))}`;
21+
return `${this.masked}${styleText('inverse', styleText('hidden', '_'))}`;
2222
}
2323
const masked = this.masked;
2424
const s1 = masked.slice(0, this.cursor);
2525
const s2 = masked.slice(this.cursor);
26-
return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
26+
return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`;
2727
}
2828
clear() {
2929
this._clearUserInput();

packages/core/src/prompts/text.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import color from 'picocolors';
1+
// import color from 'picocolors';
2+
import { styleText } from 'node:util';
23
import Prompt, { type PromptOptions } from './prompt.js';
34

45
interface TextOptions extends PromptOptions<string, TextPrompt> {
@@ -17,7 +18,7 @@ export default class TextPrompt extends Prompt<string> {
1718
}
1819
const s1 = userInput.slice(0, this.cursor);
1920
const [s2, ...s3] = userInput.slice(this.cursor);
20-
return `${s1}${color.inverse(s2)}${s3.join('')}`;
21+
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
2122
}
2223
get cursor() {
2324
return this._cursor;

packages/core/test/prompts/password.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import { cursor } from 'sisteransi';
33
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
44
import { default as PasswordPrompt } from '../../src/prompts/password.js';
@@ -65,7 +65,9 @@ describe('PasswordPrompt', () => {
6565
});
6666
instance.prompt();
6767
input.emit('keypress', 'x', { name: 'x' });
68-
expect(instance.userInputWithCursor).to.equal(`•${color.inverse(color.hidden('_'))}`);
68+
expect(instance.userInputWithCursor).to.equal(
69+
`•${styleText('inverse', styleText('hidden', '_'))}`
70+
);
6971
});
7072

7173
test('renders cursor inside value', () => {
@@ -80,7 +82,7 @@ describe('PasswordPrompt', () => {
8082
input.emit('keypress', 'z', { name: 'z' });
8183
input.emit('keypress', 'left', { name: 'left' });
8284
input.emit('keypress', 'left', { name: 'left' });
83-
expect(instance.userInputWithCursor).to.equal(`•${color.inverse('•')}•`);
85+
expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`);
8486
});
8587

8688
test('renders custom mask', () => {
@@ -92,7 +94,9 @@ describe('PasswordPrompt', () => {
9294
});
9395
instance.prompt();
9496
input.emit('keypress', 'x', { name: 'x' });
95-
expect(instance.userInputWithCursor).to.equal(`X${color.inverse(color.hidden('_'))}`);
97+
expect(instance.userInputWithCursor).to.equal(
98+
`X${styleText('inverse', styleText('hidden', '_'))}`
99+
);
96100
});
97101
});
98102
});

packages/core/test/prompts/text.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import color from 'picocolors';
1+
import { styleText } from 'node:util';
22
import { cursor } from 'sisteransi';
33
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
44
import { default as TextPrompt } from '../../src/prompts/text.js';
@@ -93,7 +93,7 @@ describe('TextPrompt', () => {
9393
input.emit('keypress', keys[i], { name: keys[i] });
9494
}
9595
input.emit('keypress', 'left', { name: 'left' });
96-
expect(instance.userInputWithCursor).to.equal(`fo${color.inverse('o')}`);
96+
expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`);
9797
});
9898

9999
test('shows cursor at end if beyond value', () => {

packages/prompts/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
},
5555
"dependencies": {
5656
"@clack/core": "workspace:*",
57-
"picocolors": "^1.0.0",
5857
"sisteransi": "^1.0.5"
5958
},
6059
"devDependencies": {

packages/prompts/src/autocomplete.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { styleText } from 'node:util';
12
import { AutocompletePrompt } from '@clack/core';
2-
import color from 'picocolors';
33
import {
44
type CommonOptions,
55
S_BAR,
@@ -89,7 +89,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
8989
validate: opts.validate,
9090
render() {
9191
// Title and message display
92-
const headings = [`${color.gray(S_BAR)}`, `${symbol(this.state)} ${opts.message}`];
92+
const headings = [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`];
9393
const userInput = this.userInput;
9494
const valueAsString = String(this.value ?? '');
9595
const options = this.options;
@@ -102,59 +102,64 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
102102
// Show selected value
103103
const selected = getSelectedOptions(this.selectedValues, options);
104104
const label =
105-
selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : '';
106-
return `${headings.join('\n')}\n${color.gray(S_BAR)}${label}`;
105+
selected.length > 0 ? ` ${styleText('dim', selected.map(getLabel).join(', '))}` : '';
106+
return `${headings.join('\n')}\n${styleText('gray', S_BAR)}${label}`;
107107
}
108108

109109
case 'cancel': {
110-
const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : '';
111-
return `${headings.join('\n')}\n${color.gray(S_BAR)}${userInputText}`;
110+
const userInputText = userInput
111+
? ` ${styleText('strikethrough', styleText('dim', userInput))}`
112+
: '';
113+
return `${headings.join('\n')}\n${styleText('gray', S_BAR)}${userInputText}`;
112114
}
113115

114116
default: {
115117
// Display cursor position - show plain text in navigation mode
116118
let searchText = '';
117119
if (this.isNavigating || showPlaceholder) {
118120
const searchTextValue = showPlaceholder ? placeholder : userInput;
119-
searchText = searchTextValue !== '' ? ` ${color.dim(searchTextValue)}` : '';
121+
searchText = searchTextValue !== '' ? ` ${styleText('dim', searchTextValue)}` : '';
120122
} else {
121123
searchText = ` ${this.userInputWithCursor}`;
122124
}
123125

124126
// Show match count if filtered
125127
const matches =
126128
this.filteredOptions.length !== options.length
127-
? color.dim(
129+
? styleText(
130+
'dim',
128131
` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
129132
)
130133
: '';
131134

132135
// No matches message
133136
const noResults =
134137
this.filteredOptions.length === 0 && userInput
135-
? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`]
138+
? [`${styleText('cyan', S_BAR)} ${styleText('yellow', 'No matches found')}`]
136139
: [];
137140

138141
const validationError =
139-
this.state === 'error' ? [`${color.yellow(S_BAR)} ${color.yellow(this.error)}`] : [];
142+
this.state === 'error'
143+
? [`${styleText('yellow', S_BAR)} ${styleText('yellow', this.error)}`]
144+
: [];
140145

141146
headings.push(
142-
`${color.cyan(S_BAR)}`,
143-
`${color.cyan(S_BAR)} ${color.dim('Search:')}${searchText}${matches}`,
147+
`${styleText('cyan', S_BAR)}`,
148+
`${styleText('cyan', S_BAR)} ${styleText('dim', 'Search:')}${searchText}${matches}`,
144149
...noResults,
145150
...validationError
146151
);
147152

148153
// Show instructions
149154
const instructions = [
150-
`${color.dim('↑/↓')} to select`,
151-
`${color.dim('Enter:')} confirm`,
152-
`${color.dim('Type:')} to search`,
155+
`${styleText('dim', '↑/↓')} to select`,
156+
`${styleText('dim', 'Enter:')} confirm`,
157+
`${styleText('dim', 'Type:')} to search`,
153158
];
154159

155160
const footers = [
156-
`${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`,
157-
`${color.cyan(S_BAR_END)}`,
161+
`${styleText('cyan', S_BAR)} ${styleText('dim', instructions.join(' • '))}`,
162+
`${styleText('cyan', S_BAR_END)}`,
158163
];
159164

160165
// Render options with selection
@@ -170,12 +175,12 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
170175
const label = getLabel(option);
171176
const hint =
172177
option.hint && option.value === this.focusedValue
173-
? color.dim(` (${option.hint})`)
178+
? styleText('dim', ` (${option.hint})`)
174179
: '';
175180

176181
return active
177-
? `${color.green(S_RADIO_ACTIVE)} ${label}${hint}`
178-
: `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}${hint}`;
182+
? `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}`
183+
: `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}${hint}`;
179184
},
180185
maxItems: opts.maxItems,
181186
output: opts.output,
@@ -184,7 +189,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
184189
// Return the formatted prompt
185190
return [
186191
...headings,
187-
...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`),
192+
...displayOptions.map((option) => `${styleText('cyan', S_BAR)} ${option}`),
188193
...footers,
189194
].join('\n');
190195
}
@@ -222,14 +227,16 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
222227
const label = option.label ?? String(option.value ?? '');
223228
const hint =
224229
option.hint && focusedValue !== undefined && option.value === focusedValue
225-
? color.dim(` (${option.hint})`)
230+
? styleText('dim', ` (${option.hint})`)
226231
: '';
227-
const checkbox = isSelected ? color.green(S_CHECKBOX_SELECTED) : color.dim(S_CHECKBOX_INACTIVE);
232+
const checkbox = isSelected
233+
? styleText('green', S_CHECKBOX_SELECTED)
234+
: styleText('dim', S_CHECKBOX_INACTIVE);
228235

229236
if (active) {
230237
return `${checkbox} ${label}${hint}`;
231238
}
232-
return `${checkbox} ${color.dim(label)}`;
239+
return `${checkbox} ${styleText('dim', label)}`;
233240
};
234241

235242
// Create text prompt which we'll use as foundation
@@ -251,7 +258,7 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
251258
output: opts.output,
252259
render() {
253260
// Title and symbol
254-
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
261+
const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
255262

256263
// Selection counter
257264
const userInput = this.userInput;
@@ -261,43 +268,46 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
261268
// Search input display
262269
const searchText =
263270
this.isNavigating || showPlaceholder
264-
? color.dim(showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode
271+
? styleText('dim', showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode
265272
: this.userInputWithCursor;
266273

267274
const options = this.options;
268275

269276
const matches =
270277
this.filteredOptions.length !== options.length
271-
? color.dim(
278+
? styleText(
279+
'dim',
272280
` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
273281
)
274282
: '';
275283

276284
// Render prompt state
277285
switch (this.state) {
278286
case 'submit': {
279-
return `${title}${color.gray(S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
287+
return `${title}${styleText('gray', S_BAR)} ${styleText('dim', `${this.selectedValues.length} items selected`)}`;
280288
}
281289
case 'cancel': {
282-
return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
290+
return `${title}${styleText('gray', S_BAR)} ${styleText('strikethrough', styleText('dim', userInput))}`;
283291
}
284292
default: {
285293
// Instructions
286294
const instructions = [
287-
`${color.dim('↑/↓')} to navigate`,
288-
`${color.dim(this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
289-
`${color.dim('Enter:')} confirm`,
290-
`${color.dim('Type:')} to search`,
295+
`${styleText('dim', '↑/↓')} to navigate`,
296+
`${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
297+
`${styleText('dim', 'Enter:')} confirm`,
298+
`${styleText('dim', 'Type:')} to search`,
291299
];
292300

293301
// No results message
294302
const noResults =
295303
this.filteredOptions.length === 0 && userInput
296-
? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`]
304+
? [`${styleText('cyan', S_BAR)} ${styleText('yellow', 'No matches found')}`]
297305
: [];
298306

299307
const errorMessage =
300-
this.state === 'error' ? [`${color.cyan(S_BAR)} ${color.yellow(this.error)}`] : [];
308+
this.state === 'error'
309+
? [`${styleText('cyan', S_BAR)} ${styleText('yellow', this.error)}`]
310+
: [];
301311

302312
// Get limited options for display
303313
const displayOptions = limitOptions({
@@ -312,12 +322,12 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
312322
// Build the prompt display
313323
return [
314324
title,
315-
`${color.cyan(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`,
325+
`${styleText('cyan', S_BAR)} ${styleText('dim', 'Search:')} ${searchText}${matches}`,
316326
...noResults,
317327
...errorMessage,
318-
...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`),
319-
`${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`,
320-
`${color.cyan(S_BAR_END)}`,
328+
...displayOptions.map((option) => `${styleText('cyan', S_BAR)} ${option}`),
329+
`${styleText('cyan', S_BAR)} ${styleText('dim', instructions.join(' • '))}`,
330+
`${styleText('cyan', S_BAR_END)}`,
321331
].join('\n');
322332
}
323333
}

packages/prompts/src/common.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Readable, Writable } from 'node:stream';
2+
import { styleText } from 'node:util';
23
import type { State } from '@clack/core';
34
import isUnicodeSupported from 'is-unicode-supported';
4-
import color from 'picocolors';
55

66
export const unicode = isUnicodeSupported();
77
export const isCI = (): boolean => process.env.CI === 'true';
@@ -43,13 +43,13 @@ export const symbol = (state: State) => {
4343
switch (state) {
4444
case 'initial':
4545
case 'active':
46-
return color.cyan(S_STEP_ACTIVE);
46+
return styleText('cyan', S_STEP_ACTIVE);
4747
case 'cancel':
48-
return color.red(S_STEP_CANCEL);
48+
return styleText('red', S_STEP_CANCEL);
4949
case 'error':
50-
return color.yellow(S_STEP_ERROR);
50+
return styleText('yellow', S_STEP_ERROR);
5151
case 'submit':
52-
return color.green(S_STEP_SUBMIT);
52+
return styleText('green', S_STEP_SUBMIT);
5353
}
5454
};
5555

0 commit comments

Comments
 (0)