···11+---
22+"@clack/prompts": minor
33+---
44+55+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.
66+77+Example usage:
88+```typescript
99+import { text } from '@clack/prompts';
1010+import color from 'picocolors';
1111+1212+const result = await text({
1313+ message: 'Enter your name',
1414+ theme: {
1515+ formatSymbolActive: (str) => color.magenta(str),
1616+ formatGuide: (str) => color.blue(str),
1717+ formatErrorMessage: (str) => color.bgRed(color.white(str)),
1818+ }
1919+});
2020+```
2121+2222+Available theme options for text prompt:
2323+- `formatSymbolActive` - Format the prompt symbol in active/initial state
2424+- `formatSymbolSubmit` - Format the prompt symbol on submit
2525+- `formatSymbolCancel` - Format the prompt symbol on cancel
2626+- `formatSymbolError` - Format the prompt symbol on error
2727+- `formatErrorMessage` - Format error messages
2828+- `formatGuide` - Format the left guide line in active state
2929+- `formatGuideSubmit` - Format the guide line on submit
3030+- `formatGuideCancel` - Format the guide line on cancel
3131+- `formatGuideError` - Format the guide line on error
3232+3333+This establishes the foundation for theming support that will be extended to other prompts.
3434+
+35
packages/prompts/src/common.ts
···7373 signal?: AbortSignal;
7474 withGuide?: boolean;
7575}
7676+7777+export type ColorFormatter = (str: string) => string;
7878+7979+/**
8080+ * Global theme options shared across all prompts.
8181+ * These control the common visual elements like the guide line.
8282+ */
8383+export interface GlobalTheme {
8484+ /** Format the left guide/border line (default: cyan) */
8585+ formatGuide?: ColorFormatter;
8686+ /** Format the guide line on submit (default: gray) */
8787+ formatGuideSubmit?: ColorFormatter;
8888+ /** Format the guide line on cancel (default: gray) */
8989+ formatGuideCancel?: ColorFormatter;
9090+ /** Format the guide line on error (default: yellow) */
9191+ formatGuideError?: ColorFormatter;
9292+}
9393+9494+export interface ThemeOptions<T> {
9595+ theme?: T & GlobalTheme;
9696+}
9797+9898+export const defaultGlobalTheme: Required<GlobalTheme> = {
9999+ formatGuide: color.cyan,
100100+ formatGuideSubmit: color.gray,
101101+ formatGuideCancel: color.gray,
102102+ formatGuideError: color.yellow,
103103+};
104104+105105+export function resolveTheme<T>(
106106+ theme: Partial<T> | undefined,
107107+ defaults: T
108108+): T {
109109+ return { ...defaults, ...theme };
110110+}
+68-10
packages/prompts/src/text.ts
···11import { settings, TextPrompt } from '@clack/core';
22import color from 'picocolors';
33-import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js';
33+import {
44+ type ColorFormatter,
55+ type CommonOptions,
66+ defaultGlobalTheme,
77+ type GlobalTheme,
88+ resolveTheme,
99+ S_BAR,
1010+ S_BAR_END,
1111+ S_STEP_ACTIVE,
1212+ S_STEP_CANCEL,
1313+ S_STEP_ERROR,
1414+ S_STEP_SUBMIT,
1515+ type ThemeOptions,
1616+} from './common.js';
1717+1818+/**
1919+ * Theme options specific to the text prompt.
2020+ * All formatters are optional - defaults will be used if not provided.
2121+ */
2222+export interface TextTheme {
2323+ /** Format the prompt symbol in active/initial state (default: cyan) */
2424+ formatSymbolActive?: ColorFormatter;
2525+ /** Format the prompt symbol on submit (default: green) */
2626+ formatSymbolSubmit?: ColorFormatter;
2727+ /** Format the prompt symbol on cancel (default: red) */
2828+ formatSymbolCancel?: ColorFormatter;
2929+ /** Format the prompt symbol on error (default: yellow) */
3030+ formatSymbolError?: ColorFormatter;
3131+ /** Format error messages (default: yellow) */
3232+ formatErrorMessage?: ColorFormatter;
3333+}
43455-export interface TextOptions extends CommonOptions {
3535+/** Default theme values for the text prompt */
3636+const defaultTextTheme: Required<TextTheme & GlobalTheme> = {
3737+ ...defaultGlobalTheme,
3838+ formatSymbolActive: color.cyan,
3939+ formatSymbolSubmit: color.green,
4040+ formatSymbolCancel: color.red,
4141+ formatSymbolError: color.yellow,
4242+ formatErrorMessage: color.yellow,
4343+};
4444+4545+export interface TextOptions extends CommonOptions, ThemeOptions<TextTheme> {
646 message: string;
747 placeholder?: string;
848 defaultValue?: string;
···1151}
12521353export const text = (opts: TextOptions) => {
5454+ const theme = resolveTheme<Required<TextTheme & GlobalTheme>>(opts.theme, defaultTextTheme);
5555+1456 return new TextPrompt({
1557 validate: opts.validate,
1658 placeholder: opts.placeholder,
···2163 input: opts.input,
2264 render() {
2365 const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false;
2424- const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbol(this.state)} `;
6666+6767+ // Resolve symbol based on state
6868+ const symbolText = (() => {
6969+ switch (this.state) {
7070+ case 'initial':
7171+ case 'active':
7272+ return theme.formatSymbolActive(S_STEP_ACTIVE);
7373+ case 'cancel':
7474+ return theme.formatSymbolCancel(S_STEP_CANCEL);
7575+ case 'error':
7676+ return theme.formatSymbolError(S_STEP_ERROR);
7777+ case 'submit':
7878+ return theme.formatSymbolSubmit(S_STEP_SUBMIT);
7979+ }
8080+ })();
8181+8282+ 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))
···31893290 switch (this.state) {
3391 case 'error': {
3434- const errorText = this.error ? ` ${color.yellow(this.error)}` : '';
3535- const errorPrefix = hasGuide ? `${color.yellow(S_BAR)} ` : '';
3636- const errorPrefixEnd = hasGuide ? color.yellow(S_BAR_END) : '';
9292+ const errorText = this.error ? ` ${theme.formatErrorMessage(this.error)}` : '';
9393+ const errorPrefix = hasGuide ? `${theme.formatGuideError(S_BAR)} ` : '';
9494+ 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)}` : '';
4141- const submitPrefix = hasGuide ? color.gray(S_BAR) : '';
9999+ 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))}` : '';
4646- const cancelPrefix = hasGuide ? color.gray(S_BAR) : '';
104104+ const cancelPrefix = hasGuide ? theme.formatGuideCancel(S_BAR) : '';
47105 return `${title}${cancelPrefix}${valueText}${value.trim() ? `\n${cancelPrefix}` : ''}`;
48106 }
49107 default: {
5050- const defaultPrefix = hasGuide ? `${color.cyan(S_BAR)} ` : '';
5151- const defaultPrefixEnd = hasGuide ? color.cyan(S_BAR_END) : '';
108108+ const defaultPrefix = hasGuide ? `${theme.formatGuide(S_BAR)} ` : '';
109109+ const defaultPrefixEnd = hasGuide ? theme.formatGuide(S_BAR_END) : '';
52110 return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`;
53111 }
54112 }