feat(ask): refine inline answer and navigation UX

This commit is contained in:
Erik Kotsiuba
2026-04-18 21:14:06 +02:00
parent 87cbfa546e
commit 91f28752a4
12 changed files with 337 additions and 53 deletions
+44 -20
View File
@@ -15,7 +15,7 @@ import {
mergeColumns,
pushWrappedText,
renderEditorBlock,
renderHint,
renderInputLine,
renderPreviewPaneContent,
} from "./render-helpers.ts";
import type {
@@ -39,7 +39,7 @@ export function renderQuestionScreen(context: QuestionRenderContext) {
}
function renderQuestionNote(context: QuestionRenderContext) {
const { lines, state, question, theme, width, editor, newLineHint } = context;
const { lines, state, question, theme, width, editor } = context;
if (isQuestionNoteOpen(state, question.id)) {
pushWrappedText(
lines,
@@ -62,7 +62,8 @@ function renderQuestionNote(context: QuestionRenderContext) {
theme,
indent: " ",
availableWidth: width - 3,
hint: renderHint(UI_TEXT.hintSave, newLineHint),
placeholder: UI_TEXT.editorPlaceholderNote,
isEmpty: editor.getText().length === 0,
});
lines.push("");
return;
@@ -105,6 +106,10 @@ function renderStandardOption(
const pointer = selected ? "❯ " : " ";
const prefix = getOptionPrefix(question.type, option, isAnsweredOption);
const optionColor = getOptionColor(isAnsweredOption, selected);
if (option.isCustomOption && selected && isInputOpenForQuestion(state, question.id)) {
renderInteractiveCustomOption(context, index, pointer, prefix, optionColor);
return;
}
pushWrappedText(
lines,
@@ -321,25 +326,13 @@ function renderOptionEditor(
context: OptionDetailRenderContext,
renderState: ReturnType<typeof getOptionDetailRenderState>,
): boolean {
const { lines, editor, width, theme, newLineHint } = context;
const { lines, editor, width, theme } = context;
if (renderState.noteOpen) {
renderIndentedEditor(
lines,
editor,
width,
theme,
renderHint(UI_TEXT.hintSave, newLineHint),
);
renderIndentedEditor(lines, editor, width, theme, UI_TEXT.editorPlaceholderNote);
return true;
}
if (renderState.inputOpen) {
renderIndentedEditor(
lines,
editor,
width,
theme,
renderHint(UI_TEXT.hintSubmit, newLineHint),
);
renderIndentedEditor(lines, editor, width, theme, UI_TEXT.editorPlaceholderInput);
return true;
}
return false;
@@ -389,7 +382,7 @@ function renderIndentedEditor(
editor: QuestionRenderContext["editor"],
width: number,
theme: Theme,
hint: string,
placeholder: string,
) {
renderEditorBlock({
lines,
@@ -403,7 +396,8 @@ function renderIndentedEditor(
theme,
indent: " ",
availableWidth: width - 5,
hint,
placeholder,
isEmpty: editor.getText().length === 0,
});
}
@@ -418,6 +412,36 @@ function getOptionPrefix(
return `[${isAnsweredOption ? "x" : " "}] `;
}
function renderInteractiveCustomOption(
context: QuestionRenderContext,
index: number,
pointer: string,
prefix: string,
optionColor: ReturnType<typeof getOptionColor>,
) {
const { lines, editor, theme, width } = context;
const label = `${index + 1}. ${prefix}`;
const availableWidth = Math.max(
1,
width - visibleWidth(pointer) - visibleWidth(label),
);
const text = editor.getText().replace(/\n/g, " ");
const hasText = text.trim().length > 0;
const inputText = hasText ? text : UI_TEXT.editorPlaceholderInput;
const inputColor = hasText ? "text" : "muted";
lines.push(
truncateToWidth(
`${pointer}${theme.fg(optionColor, label)}${renderInputLine(
inputText,
availableWidth,
theme,
inputColor,
)}`,
width,
),
);
}
function getOptionColor(isAnsweredOption: boolean, selected: boolean) {
if (isAnsweredOption) {
return "success";