fix: improve custom input editor rendering
This commit is contained in:
@@ -63,7 +63,6 @@ export function renderEditorBlock(args: {
|
|||||||
availableWidth,
|
availableWidth,
|
||||||
placeholder,
|
placeholder,
|
||||||
placeholderColor = "muted",
|
placeholderColor = "muted",
|
||||||
contentColor = "text",
|
|
||||||
isEmpty = false,
|
isEmpty = false,
|
||||||
} = args;
|
} = args;
|
||||||
const innerLines =
|
const innerLines =
|
||||||
@@ -87,18 +86,47 @@ export function renderEditorBlock(args: {
|
|||||||
for (const editorLine of innerLines) {
|
for (const editorLine of innerLines) {
|
||||||
lines.push(
|
lines.push(
|
||||||
truncateToWidth(
|
truncateToWidth(
|
||||||
`${indent}${renderInputLine(
|
`${indent}${renderEditorLine(editorLine, availableWidth, theme)}`,
|
||||||
editorLine,
|
|
||||||
availableWidth,
|
|
||||||
theme,
|
|
||||||
contentColor
|
|
||||||
)}`,
|
|
||||||
width
|
width
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEditorLine(
|
||||||
|
line: string,
|
||||||
|
availableWidth: number,
|
||||||
|
theme: Theme
|
||||||
|
): string {
|
||||||
|
const innerWidth = Math.max(4, availableWidth - 2);
|
||||||
|
const truncated = truncateToWidth(line, innerWidth);
|
||||||
|
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(truncated)));
|
||||||
|
return renderPersistentBackground(
|
||||||
|
`${truncated}${padding}`,
|
||||||
|
theme,
|
||||||
|
"selectedBg"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersistentBackground(
|
||||||
|
text: string,
|
||||||
|
theme: Theme,
|
||||||
|
background: "selectedBg"
|
||||||
|
): string {
|
||||||
|
const marker = "__PI_BG_MARKER__";
|
||||||
|
const wrappedMarker = theme.bg(background, marker);
|
||||||
|
const markerIndex = wrappedMarker.indexOf(marker);
|
||||||
|
if (markerIndex === -1) {
|
||||||
|
return theme.bg(background, ` ${text} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = wrappedMarker.slice(0, markerIndex);
|
||||||
|
const suffix = wrappedMarker.slice(markerIndex + marker.length);
|
||||||
|
const ansiReset = "\u001b[0m";
|
||||||
|
const reopenedText = text.split(ansiReset).join(`${ansiReset}${prefix}`);
|
||||||
|
return `${prefix} ${reopenedText} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderBox(
|
export function renderBox(
|
||||||
content: Array<{ text: string; color: ThemeColor }>,
|
content: Array<{ text: string; color: ThemeColor }>,
|
||||||
width: number,
|
width: number,
|
||||||
|
|||||||
+14
-19
@@ -15,7 +15,6 @@ import {
|
|||||||
mergeColumns,
|
mergeColumns,
|
||||||
pushWrappedText,
|
pushWrappedText,
|
||||||
renderEditorBlock,
|
renderEditorBlock,
|
||||||
renderInputLine,
|
|
||||||
renderPreviewPaneContent,
|
renderPreviewPaneContent,
|
||||||
} from "./render-helpers.ts";
|
} from "./render-helpers.ts";
|
||||||
import type {
|
import type {
|
||||||
@@ -436,25 +435,21 @@ function renderInteractiveCustomOption(
|
|||||||
optionColor: ReturnType<typeof getOptionColor>
|
optionColor: ReturnType<typeof getOptionColor>
|
||||||
) {
|
) {
|
||||||
const { lines, editor, theme, width } = context;
|
const { lines, editor, theme, width } = context;
|
||||||
const label = `${index + 1}. ${prefix}`;
|
pushWrappedText(
|
||||||
const availableWidth = Math.max(
|
lines,
|
||||||
1,
|
`${index + 1}. ${prefix}Type your own`,
|
||||||
width - visibleWidth(pointer) - visibleWidth(label)
|
width,
|
||||||
|
theme,
|
||||||
|
optionColor,
|
||||||
|
pointer,
|
||||||
|
" ".repeat(visibleWidth(pointer))
|
||||||
);
|
);
|
||||||
const text = editor.getText().replace(/\n/g, " ");
|
renderIndentedEditor(
|
||||||
const hasText = text.trim().length > 0;
|
lines,
|
||||||
const inputText = hasText ? text : UI_TEXT.editorPlaceholderInput;
|
editor,
|
||||||
const inputColor = hasText ? "text" : "muted";
|
width,
|
||||||
lines.push(
|
theme,
|
||||||
truncateToWidth(
|
UI_TEXT.editorPlaceholderInput
|
||||||
`${pointer}${theme.fg(optionColor, label)}${renderInputLine(
|
|
||||||
inputText,
|
|
||||||
availableWidth,
|
|
||||||
theme,
|
|
||||||
inputColor
|
|
||||||
)}`,
|
|
||||||
width
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { renderEditorBlock } from "../src/ui/render-helpers.ts";
|
|||||||
const ABC_PATTERN = /abc/;
|
const ABC_PATTERN = /abc/;
|
||||||
const BORDER_PATTERN = /┌|└/;
|
const BORDER_PATTERN = /┌|└/;
|
||||||
const TYPE_YOUR_PATTERN = /Type your/;
|
const TYPE_YOUR_PATTERN = /Type your/;
|
||||||
|
const SELECTED_BG_PATTERN = /\{selectedBg\}/;
|
||||||
|
|
||||||
function mockTheme() {
|
function mockTheme() {
|
||||||
return {
|
return {
|
||||||
@@ -21,7 +22,7 @@ test("renderEditorBlock strips editor borders", () => {
|
|||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
renderEditorBlock({
|
renderEditorBlock({
|
||||||
lines,
|
lines,
|
||||||
editorLines: ["┌──┐", "│abc│", "└──┘"],
|
editorLines: ["┌──┐", "abc", "└──┘"],
|
||||||
width: 40,
|
width: 40,
|
||||||
theme: mockTheme() as never,
|
theme: mockTheme() as never,
|
||||||
indent: " ",
|
indent: " ",
|
||||||
@@ -30,6 +31,7 @@ test("renderEditorBlock strips editor borders", () => {
|
|||||||
|
|
||||||
assert.equal(lines.length, 1);
|
assert.equal(lines.length, 1);
|
||||||
assert.match(lines[0], ABC_PATTERN);
|
assert.match(lines[0], ABC_PATTERN);
|
||||||
|
assert.match(lines[0], SELECTED_BG_PATTERN);
|
||||||
assert.doesNotMatch(lines[0], BORDER_PATTERN);
|
assert.doesNotMatch(lines[0], BORDER_PATTERN);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ test("renderEditorBlock shows a muted placeholder when empty", () => {
|
|||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
renderEditorBlock({
|
renderEditorBlock({
|
||||||
lines,
|
lines,
|
||||||
editorLines: ["┌──┐", "│ │", "└──┘"],
|
editorLines: ["┌──┐", " ", "└──┘"],
|
||||||
width: 40,
|
width: 40,
|
||||||
theme: mockTheme() as never,
|
theme: mockTheme() as never,
|
||||||
indent: " ",
|
indent: " ",
|
||||||
@@ -48,5 +50,22 @@ test("renderEditorBlock shows a muted placeholder when empty", () => {
|
|||||||
|
|
||||||
assert.equal(lines.length, 1);
|
assert.equal(lines.length, 1);
|
||||||
assert.match(lines[0], TYPE_YOUR_PATTERN);
|
assert.match(lines[0], TYPE_YOUR_PATTERN);
|
||||||
|
assert.match(lines[0], SELECTED_BG_PATTERN);
|
||||||
assert.doesNotMatch(lines[0], BORDER_PATTERN);
|
assert.doesNotMatch(lines[0], BORDER_PATTERN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("renderEditorBlock reapplies background after editor reset sequences", () => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
renderEditorBlock({
|
||||||
|
lines,
|
||||||
|
editorLines: ["┌──┐", "abc\x1b[7m \x1b[0m", "└──┘"],
|
||||||
|
width: 40,
|
||||||
|
theme: mockTheme() as never,
|
||||||
|
indent: " ",
|
||||||
|
availableWidth: 40,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(lines.length, 1);
|
||||||
|
assert.match(lines[0], SELECTED_BG_PATTERN);
|
||||||
|
assert(lines[0].includes("\x1b[0m{selectedBg}"));
|
||||||
|
});
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ function mockTheme() {
|
|||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockEditor(text = "") {
|
function mockEditor(text = "", renderedLines?: string[]) {
|
||||||
return {
|
return {
|
||||||
getText() {
|
getText() {
|
||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return [];
|
return renderedLines ?? [];
|
||||||
},
|
},
|
||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ test("custom option stays labeled before selection", () => {
|
|||||||
assert(!lines.some((line) => line.includes("Type your answer...")));
|
assert(!lines.some((line) => line.includes("Type your answer...")));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selected custom option becomes inline input", () => {
|
test("selected custom option keeps its label and renders editor below", () => {
|
||||||
let state = createInitialState({
|
let state = createInitialState({
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@ test("selected custom option becomes inline input", () => {
|
|||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
renderQuestionScreen({
|
renderQuestionScreen({
|
||||||
editor: mockEditor(""),
|
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
|
||||||
lines,
|
lines,
|
||||||
options: getRenderableOptions(state.questions[0]),
|
options: getRenderableOptions(state.questions[0]),
|
||||||
question: state.questions[0],
|
question: state.questions[0],
|
||||||
@@ -79,6 +79,40 @@ test("selected custom option becomes inline input", () => {
|
|||||||
width: 80,
|
width: 80,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert(lines.some((line) => line.includes("Type your own")));
|
||||||
assert(lines.some((line) => line.includes("Type your answer...")));
|
assert(lines.some((line) => line.includes("Type your answer...")));
|
||||||
assert(!lines.some((line) => line.includes("Type your own answer")));
|
});
|
||||||
|
|
||||||
|
test("selected multiline custom option renders full editor block", () => {
|
||||||
|
let state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
prompt: "Pick one",
|
||||||
|
options: [{ value: "a", label: "A" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
state = applyNumberShortcut(state, 2);
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
renderQuestionScreen({
|
||||||
|
editor: mockEditor("first line\nsecond line", [
|
||||||
|
"┌────┐",
|
||||||
|
"first line",
|
||||||
|
"second line",
|
||||||
|
"└────┘",
|
||||||
|
]),
|
||||||
|
lines,
|
||||||
|
options: getRenderableOptions(state.questions[0]),
|
||||||
|
question: state.questions[0],
|
||||||
|
state,
|
||||||
|
theme: mockTheme(),
|
||||||
|
width: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(lines.some((line) => line.includes("Type your own")));
|
||||||
|
assert(lines.some((line) => line.includes("first line")));
|
||||||
|
assert(lines.some((line) => line.includes("second line")));
|
||||||
|
assert(!lines.some((line) => line.includes("first line second line")));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user