From b59c967d985a6f62dc32da3b4f2c5767d01608a9 Mon Sep 17 00:00:00 2001 From: Erik Kotsiuba Date: Sat, 25 Apr 2026 18:30:54 +0200 Subject: [PATCH] feat(ui): split submit tab into actions and review --- src/ui/constants.ts | 2 ++ src/ui/render-submit.ts | 62 ++++++++++++++++++++++++++++++++++--- tests/render-submit.test.ts | 61 ++++++++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 7 deletions(-) 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")