Skip to content

Commit a43ce24

Browse files
committed
feat(@clack/prompts): add theme support for text prompt customization
1 parent 9b92161 commit a43ce24

File tree

5 files changed

+448
-10
lines changed

5 files changed

+448
-10
lines changed

.changeset/dark-roses-report.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@clack/prompts": minor
3+
---
4+
5+
Add theme support for the text prompt. Users can now customize the colors of symbols, guide lines, and error messages by passing a `theme` option.
6+
7+
Example usage:
8+
```typescript
9+
import { text } from '@clack/prompts';
10+
import color from 'picocolors';
11+
12+
const result = await text({
13+
message: 'Enter your name',
14+
theme: {
15+
formatSymbolActive: (str) => color.magenta(str),
16+
formatGuide: (str) => color.blue(str),
17+
formatErrorMessage: (str) => color.bgRed(color.white(str)),
18+
}
19+
});
20+
```
21+
22+
Available theme options for text prompt:
23+
- `formatSymbolActive` - Format the prompt symbol in active/initial state
24+
- `formatSymbolSubmit` - Format the prompt symbol on submit
25+
- `formatSymbolCancel` - Format the prompt symbol on cancel
26+
- `formatSymbolError` - Format the prompt symbol on error
27+
- `formatErrorMessage` - Format error messages
28+
- `formatGuide` - Format the left guide line in active state
29+
- `formatGuideSubmit` - Format the guide line on submit
30+
- `formatGuideCancel` - Format the guide line on cancel
31+
- `formatGuideError` - Format the guide line on error
32+
33+
This establishes the foundation for theming support that will be extended to other prompts.
34+

packages/prompts/src/common.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,38 @@ export interface CommonOptions {
7373
signal?: AbortSignal;
7474
withGuide?: boolean;
7575
}
76+
77+
export type ColorFormatter = (str: string) => string;
78+
79+
/**
80+
* Global theme options shared across all prompts.
81+
* These control the common visual elements like the guide line.
82+
*/
83+
export interface GlobalTheme {
84+
/** Format the left guide/border line (default: cyan) */
85+
formatGuide?: ColorFormatter;
86+
/** Format the guide line on submit (default: gray) */
87+
formatGuideSubmit?: ColorFormatter;
88+
/** Format the guide line on cancel (default: gray) */
89+
formatGuideCancel?: ColorFormatter;
90+
/** Format the guide line on error (default: yellow) */
91+
formatGuideError?: ColorFormatter;
92+
}
93+
94+
export interface ThemeOptions<T> {
95+
theme?: T & GlobalTheme;
96+
}
97+
98+
export const defaultGlobalTheme: Required<GlobalTheme> = {
99+
formatGuide: color.cyan,
100+
formatGuideSubmit: color.gray,
101+
formatGuideCancel: color.gray,
102+
formatGuideError: color.yellow,
103+
};
104+
105+
export function resolveTheme<T>(
106+
theme: Partial<T> | undefined,
107+
defaults: T
108+
): T {
109+
return { ...defaults, ...theme };
110+
}

packages/prompts/src/text.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
11
import { settings, TextPrompt } from '@clack/core';
22
import color from 'picocolors';
3-
import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js';
3+
import {
4+
type ColorFormatter,
5+
type CommonOptions,
6+
defaultGlobalTheme,
7+
type GlobalTheme,
8+
resolveTheme,
9+
S_BAR,
10+
S_BAR_END,
11+
S_STEP_ACTIVE,
12+
S_STEP_CANCEL,
13+
S_STEP_ERROR,
14+
S_STEP_SUBMIT,
15+
type ThemeOptions,
16+
} from './common.js';
417

5-
export interface TextOptions extends CommonOptions {
18+
/**
19+
* Theme options specific to the text prompt.
20+
* All formatters are optional - defaults will be used if not provided.
21+
*/
22+
export interface TextTheme {
23+
/** Format the prompt symbol in active/initial state (default: cyan) */
24+
formatSymbolActive?: ColorFormatter;
25+
/** Format the prompt symbol on submit (default: green) */
26+
formatSymbolSubmit?: ColorFormatter;
27+
/** Format the prompt symbol on cancel (default: red) */
28+
formatSymbolCancel?: ColorFormatter;
29+
/** Format the prompt symbol on error (default: yellow) */
30+
formatSymbolError?: ColorFormatter;
31+
/** Format error messages (default: yellow) */
32+
formatErrorMessage?: ColorFormatter;
33+
}
34+
35+
/** Default theme values for the text prompt */
36+
const defaultTextTheme: Required<TextTheme & GlobalTheme> = {
37+
...defaultGlobalTheme,
38+
formatSymbolActive: color.cyan,
39+
formatSymbolSubmit: color.green,
40+
formatSymbolCancel: color.red,
41+
formatSymbolError: color.yellow,
42+
formatErrorMessage: color.yellow,
43+
};
44+
45+
export interface TextOptions extends CommonOptions, ThemeOptions<TextTheme> {
646
message: string;
747
placeholder?: string;
848
defaultValue?: string;
@@ -11,6 +51,8 @@ export interface TextOptions extends CommonOptions {
1151
}
1252

1353
export const text = (opts: TextOptions) => {
54+
const theme = resolveTheme<Required<TextTheme & GlobalTheme>>(opts.theme, defaultTextTheme);
55+
1456
return new TextPrompt({
1557
validate: opts.validate,
1658
placeholder: opts.placeholder,
@@ -21,7 +63,23 @@ export const text = (opts: TextOptions) => {
2163
input: opts.input,
2264
render() {
2365
const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false;
24-
const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbol(this.state)} `;
66+
67+
// Resolve symbol based on state
68+
const symbolText = (() => {
69+
switch (this.state) {
70+
case 'initial':
71+
case 'active':
72+
return theme.formatSymbolActive(S_STEP_ACTIVE);
73+
case 'cancel':
74+
return theme.formatSymbolCancel(S_STEP_CANCEL);
75+
case 'error':
76+
return theme.formatSymbolError(S_STEP_ERROR);
77+
case 'submit':
78+
return theme.formatSymbolSubmit(S_STEP_SUBMIT);
79+
}
80+
})();
81+
82+
const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbolText} `;
2583
const title = `${titlePrefix}${opts.message}\n`;
2684
const placeholder = opts.placeholder
2785
? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1))
@@ -31,24 +89,24 @@ export const text = (opts: TextOptions) => {
3189

3290
switch (this.state) {
3391
case 'error': {
34-
const errorText = this.error ? ` ${color.yellow(this.error)}` : '';
35-
const errorPrefix = hasGuide ? `${color.yellow(S_BAR)} ` : '';
36-
const errorPrefixEnd = hasGuide ? color.yellow(S_BAR_END) : '';
92+
const errorText = this.error ? ` ${theme.formatErrorMessage(this.error)}` : '';
93+
const errorPrefix = hasGuide ? `${theme.formatGuideError(S_BAR)} ` : '';
94+
const errorPrefixEnd = hasGuide ? theme.formatGuideError(S_BAR_END) : '';
3795
return `${title.trim()}\n${errorPrefix}${userInput}\n${errorPrefixEnd}${errorText}\n`;
3896
}
3997
case 'submit': {
4098
const valueText = value ? ` ${color.dim(value)}` : '';
41-
const submitPrefix = hasGuide ? color.gray(S_BAR) : '';
99+
const submitPrefix = hasGuide ? theme.formatGuideSubmit(S_BAR) : '';
42100
return `${title}${submitPrefix}${valueText}`;
43101
}
44102
case 'cancel': {
45103
const valueText = value ? ` ${color.strikethrough(color.dim(value))}` : '';
46-
const cancelPrefix = hasGuide ? color.gray(S_BAR) : '';
104+
const cancelPrefix = hasGuide ? theme.formatGuideCancel(S_BAR) : '';
47105
return `${title}${cancelPrefix}${valueText}${value.trim() ? `\n${cancelPrefix}` : ''}`;
48106
}
49107
default: {
50-
const defaultPrefix = hasGuide ? `${color.cyan(S_BAR)} ` : '';
51-
const defaultPrefixEnd = hasGuide ? color.cyan(S_BAR_END) : '';
108+
const defaultPrefix = hasGuide ? `${theme.formatGuide(S_BAR)} ` : '';
109+
const defaultPrefixEnd = hasGuide ? theme.formatGuide(S_BAR_END) : '';
52110
return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`;
53111
}
54112
}

0 commit comments

Comments
 (0)