diff --git a/src/ui/constants.ts b/src/ui/constants.ts
index e39d351..5f4def4 100644
--- a/src/ui/constants.ts
+++ b/src/ui/constants.ts
@@ -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 = {
diff --git a/src/ui/render-submit.ts b/src/ui/render-submit.ts
index 175ef98..13c8849 100644
--- a/src/ui/render-submit.ts
+++ b/src/ui/render-submit.ts
@@ -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(
diff --git a/tests/render-submit.test.ts b/tests/render-submit.test.ts
index 37abf90..ca1982c 100644
--- a/tests/render-submit.test.ts
+++ b/tests/render-submit.test.ts
@@ -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("Single")
@@ -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("Note:"))
);
@@ -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")