fix: polish ask note spacing and styling

This commit is contained in:
Erik Kotsiuba
2026-04-19 16:32:27 +02:00
parent 9ab368ad6b
commit e0e2b04688
4 changed files with 143 additions and 35 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ export const UI_DIMENSIONS = {
} as const;
export const UI_TEXT = {
questionNoteTitle: "Note",
questionNoteTitle: "Note:",
reviewTitle: "Review answers",
readyToSubmit: "Submit answers?",
unanswered: "→ unanswered",
@@ -24,5 +24,5 @@ export const UI_TEXT = {
footerDefault:
" ⇆ tab · ↑↓ select · Enter confirm · N/Shift+N note · Esc dismiss",
editorPlaceholderInput: "Type answer...",
editorPlaceholderNote: "Add note...",
editorPlaceholderNote: "Add a note...",
} as const;
+53 -1
View File
@@ -5,7 +5,14 @@ import { wrapText } from "../text.ts";
import { UI_DIMENSIONS, UI_TEXT } from "./constants.ts";
type Theme = ExtensionContext["ui"]["theme"];
type ThemeColor = "accent" | "muted" | "text" | "dim" | "success" | "warning";
type ThemeColor =
| "accent"
| "muted"
| "text"
| "dim"
| "success"
| "warning"
| "syntaxString";
const EDITOR_BORDER_PATTERN = /^[┌┐└┘─]+$/;
const EDITOR_SCROLL_BORDER_PATTERN = /^─── [↑↓] \d+ more ─*$/;
@@ -97,6 +104,51 @@ export function renderEditorBlock(args: {
}
}
export function renderLabeledEditorBlock(args: {
lines: string[];
label: string;
editorLines: string[];
width: number;
theme: Theme;
indent: string;
availableWidth: number;
placeholder?: string;
placeholderColor?: ThemeColor;
isEmpty?: boolean;
}) {
const {
lines,
label,
editorLines,
width,
theme,
indent,
availableWidth,
placeholder,
placeholderColor = "muted",
isEmpty = false,
} = args;
const contentLines = getEditorContentLines(editorLines);
const labelText = theme.fg("accent", label);
const contentIndent = `${indent}${" ".repeat(visibleWidth(label) + 1)}`;
const editorWidth = Math.max(4, availableWidth - visibleWidth(label) - 1);
const firstLine =
isEmpty && placeholder
? renderInputLine(placeholder, editorWidth, theme, placeholderColor)
: renderEditorLine(contentLines[0] ?? "", editorWidth, theme);
lines.push(truncateToWidth(`${indent}${labelText} ${firstLine}`, width));
for (const editorLine of contentLines.slice(1)) {
lines.push(
truncateToWidth(
`${contentIndent}${renderEditorLine(editorLine, availableWidth, theme)}`,
width
)
);
}
}
function getEditorContentLines(editorLines: string[]): string[] {
if (editorLines.length <= 2) {
return editorLines;
+48 -30
View File
@@ -26,8 +26,10 @@ import type {
export function renderQuestionScreen(context: QuestionRenderContext) {
const { lines, question, theme, width } = context;
pushWrappedText(lines, question.prompt, width, theme, "text", " ", " ");
const renderedQuestionNote = renderQuestionNote(context);
if (!renderedQuestionNote) {
lines.push("");
renderQuestionNote(context);
}
if (question.type === "preview") {
renderPreviewQuestion(context);
@@ -37,51 +39,35 @@ export function renderQuestionScreen(context: QuestionRenderContext) {
renderStandardQuestion(context);
}
function renderQuestionNote(context: QuestionRenderContext) {
function renderQuestionNote(context: QuestionRenderContext): boolean {
const { lines, state, question, theme, width, editor } = context;
if (isQuestionNoteOpen(state, question.id)) {
pushWrappedText(
renderQuestionNoteEditor(
lines,
UI_TEXT.questionNoteTitle,
editor,
width,
theme,
"accent",
" ",
" "
UI_TEXT.editorPlaceholderNote
);
renderEditorBlock({
lines,
editorLines: editor.render(
Math.max(
UI_DIMENSIONS.editorMinWidth,
width - UI_DIMENSIONS.editorContentPadding
)
),
width,
theme,
indent: " ",
availableWidth: width - 3,
placeholder: UI_TEXT.editorPlaceholderNote,
isEmpty: editor.getText().length === 0,
});
lines.push("");
return;
return true;
}
const existingNote = getQuestionNote(state, question.id);
if (!existingNote) {
return;
return false;
}
pushWrappedText(
lines,
`Question note: ${existingNote}`,
existingNote,
width,
theme,
"muted",
" ",
" "
getSavedNotePrefix(theme, " "),
getSavedNoteContinuationPrefix(" ")
);
lines.push("");
return true;
}
function renderStandardQuestion(context: QuestionRenderContext) {
@@ -383,15 +369,39 @@ function renderSavedOptionNote(
const { lines, theme, width } = context;
pushWrappedText(
lines,
`Note: ${optionNote}`,
optionNote,
width,
theme,
"muted",
" ",
" "
getSavedNotePrefix(theme, " "),
getSavedNoteContinuationPrefix(" ")
);
}
function renderQuestionNoteEditor(
lines: string[],
editor: QuestionRenderContext["editor"],
width: number,
theme: Theme,
placeholder: string
) {
renderEditorBlock({
lines,
editorLines: editor.render(
Math.max(
UI_DIMENSIONS.editorMinWidth,
width - UI_DIMENSIONS.editorContentPadding
)
),
width,
theme,
indent: " ",
availableWidth: width - 1,
placeholder,
isEmpty: editor.getText().length === 0,
});
}
function renderIndentedEditor(
lines: string[],
editor: QuestionRenderContext["editor"],
@@ -416,6 +426,14 @@ function renderIndentedEditor(
});
}
function getSavedNotePrefix(theme: Theme, indent: string): string {
return `${indent}${theme.fg("syntaxString", UI_TEXT.questionNoteTitle)} `;
}
function getSavedNoteContinuationPrefix(indent: string): string {
return `${indent}${" ".repeat(visibleWidth(UI_TEXT.questionNoteTitle) + 1)}`;
}
function getOptionPrefix(
questionType: QuestionRenderContext["question"]["type"],
option: AskDisplayOption,
+39 -1
View File
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
import test from "node:test";
import { createInitialState } from "../src/state/create.ts";
import { getRenderableOptions } from "../src/state/selectors.ts";
import { applyNumberShortcut } from "../src/state/transitions.ts";
import {
applyNumberShortcut,
enterQuestionNoteMode,
} from "../src/state/transitions.ts";
import { renderQuestionScreen } from "../src/ui/render-question.ts";
function mockTheme() {
@@ -83,6 +86,41 @@ test("selected custom option keeps its label and renders editor below", () => {
assert(lines.some((line) => line.includes("Type answer...")));
});
test("open question note renders inline label and editor", () => {
const state = enterQuestionNoteMode(
createInitialState({
questions: [
{
id: "q1",
prompt: "Pick any extra things to include.",
options: [{ value: "a", label: "A" }],
},
],
}),
"q1"
);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
const promptIndex = lines.findIndex((line) =>
line.includes("Pick any extra things to include.")
);
const inputIndex = lines.findIndex((line) => line.includes("Add a note..."));
assert.notEqual(promptIndex, -1);
assert.equal(inputIndex, promptIndex + 1);
assert(!lines.some((line) => line.includes("Note:")));
});
test("selected multiline custom option renders full editor block", () => {
let state = createInitialState({
questions: [