feat(ui): split submit tab into actions and review

This commit is contained in:
Erik Kotsiuba
2026-04-25 20:01:43 +02:00
parent 2360012f1c
commit b59c967d98
3 changed files with 118 additions and 7 deletions
+2
View File
@@ -9,6 +9,8 @@ export const UI_DIMENSIONS = {
previewLeftMinWidth: 22,
previewLeftMaxWidth: 34,
previewLeftRatio: 0.34,
submitWideMinWidth: 64,
submitActionsWidth: 24,
} as const;
export const UI_TEXT = {
+58 -4
View File
@@ -2,8 +2,8 @@ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import { SUBMIT_CHOICES } from "../constants.ts";
import { serializeAnswer } from "../state/answers.ts";
import type { AskResult, AskState, AskStateAnswer } from "../types.ts";
import { UI_TEXT } from "./constants.ts";
import { pushWrappedText } from "./render-helpers.ts";
import { UI_DIMENSIONS, UI_TEXT } from "./constants.ts";
import { mergeColumns, pushWrappedText } from "./render-helpers.ts";
import type { Theme } from "./render-types.ts";
type ReviewAnswer = AskResult["answers"][string] & {
@@ -19,10 +19,51 @@ export function renderSubmitScreen(
theme: Theme,
width: number
) {
const showAllNotes = state.activeSubmitActionIndex === 1;
if (shouldUseWideSubmitLayout(width)) {
const leftWidth = UI_DIMENSIONS.submitActionsWidth;
const rightWidth = Math.max(1, width - leftWidth - 2);
const reviewLines = renderSubmitReviewLines(
state,
theme,
rightWidth,
showAllNotes
);
const actionLines = renderSubmitActionLines(state, theme, leftWidth);
for (const line of mergeColumns(
actionLines,
reviewLines,
leftWidth,
width
)) {
lines.push(line);
}
return;
}
const reviewLines = renderSubmitReviewLines(
state,
theme,
width,
showAllNotes
);
const actionLines = renderSubmitActionLines(state, theme, width);
lines.push(...reviewLines);
lines.push("");
lines.push(...actionLines);
}
function renderSubmitReviewLines(
state: AskState,
theme: Theme,
width: number,
showAllNotes: boolean
): string[] {
const lines: string[] = [];
pushWrappedText(lines, UI_TEXT.reviewTitle, width, theme, "accent", " ", " ");
lines.push("");
const showAllNotes = state.activeSubmitActionIndex === 1;
for (let index = 0; index < state.questions.length; index++) {
const question = state.questions[index];
renderSubmittedQuestion(
@@ -37,8 +78,21 @@ export function renderSubmitScreen(
}
}
lines.push("");
return lines;
}
function renderSubmitActionLines(
state: AskState,
theme: Theme,
width: number
): string[] {
const lines: string[] = [];
renderSubmitActions(lines, state, theme, width);
return lines;
}
function shouldUseWideSubmitLayout(width: number): boolean {
return width >= UI_DIMENSIONS.submitWideMinWidth;
}
function renderSubmittedQuestion(
+58 -3
View File
@@ -45,6 +45,61 @@ test("submit screen uses shorter action labels without extra prompt text", () =>
assert(!lines.some((line) => line.includes("1. Submit answers")));
});
test("submit screen uses side-by-side layout on wide screens", () => {
const state = createInitialState({
questions: [
{
id: "q1",
label: "Color",
prompt: "Pick one color.",
options: [{ value: "blue", label: "Blue" }],
},
],
});
const lines: string[] = [];
renderSubmitScreen(lines, state, mockTheme(), 120);
assert(
lines.some(
(line) => line.includes("1. Submit") && line.includes("Review answers")
)
);
assert(
lines.some((line) => line.includes("3. Cancel") && line.includes("Color"))
);
});
test("submit screen stacks review above actions on narrow screens", () => {
const state = createInitialState({
questions: [
{
id: "q1",
label: "Color",
prompt: "Pick one color.",
options: [{ value: "blue", label: "Blue" }],
},
],
});
const lines: string[] = [];
renderSubmitScreen(lines, state, mockTheme(), 50);
const reviewIndex = lines.findIndex((line) =>
line.includes("Review answers")
);
const actionIndex = lines.findIndex((line) => line.includes("1. Submit"));
assert.notEqual(reviewIndex, -1);
assert.notEqual(actionIndex, -1);
assert(reviewIndex < actionIndex);
assert(
!lines.some(
(line) => line.includes("1. Submit") && line.includes("Review answers")
)
);
});
test("submit screen shows notes only for answered questions in submit mode", () => {
let state = createInitialState({
questions: [
@@ -72,7 +127,7 @@ test("submit screen shows notes only for answered questions in submit mode", ()
state = saveNote(state, "Second note");
const lines: string[] = [];
renderSubmitScreen(lines, state, mockTheme(), 80);
renderSubmitScreen(lines, state, mockTheme(), 140);
const firstQuestionIndex = lines.findIndex((line) =>
line.includes("<text>Single</text>")
@@ -99,7 +154,7 @@ test("submit screen shows notes only for answered questions in submit mode", ()
assert(optionNoteIndex > firstAnswerIndex);
assert(lines[firstQuestionNoteIndex]?.startsWith(" "));
assert(lines[optionNoteIndex]?.startsWith(" "));
assert.equal(lines[secondQuestionIndex - 1], "");
assert.equal(lines[secondQuestionIndex - 1]?.trim(), "");
assert(
lines.some((line) => line.includes("<syntaxString>Note:</syntaxString>"))
);
@@ -178,7 +233,7 @@ test("submit screen renders multi-select option notes under their related answer
state = saveNote(state, "Second note");
const lines: string[] = [];
renderSubmitScreen(lines, state, mockTheme(), 80);
renderSubmitScreen(lines, state, mockTheme(), 140);
const firstAnswerIndex = lines.findIndex((line) =>
line.includes("→ Follow-up action")