fix: polish ask note spacing and styling
This commit is contained in:
+2
-2
@@ -12,7 +12,7 @@ export const UI_DIMENSIONS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const UI_TEXT = {
|
export const UI_TEXT = {
|
||||||
questionNoteTitle: "Note",
|
questionNoteTitle: "Note:",
|
||||||
reviewTitle: "Review answers",
|
reviewTitle: "Review answers",
|
||||||
readyToSubmit: "Submit answers?",
|
readyToSubmit: "Submit answers?",
|
||||||
unanswered: "→ unanswered",
|
unanswered: "→ unanswered",
|
||||||
@@ -24,5 +24,5 @@ export const UI_TEXT = {
|
|||||||
footerDefault:
|
footerDefault:
|
||||||
" ⇆ tab · ↑↓ select · Enter confirm · N/Shift+N note · Esc dismiss",
|
" ⇆ tab · ↑↓ select · Enter confirm · N/Shift+N note · Esc dismiss",
|
||||||
editorPlaceholderInput: "Type answer...",
|
editorPlaceholderInput: "Type answer...",
|
||||||
editorPlaceholderNote: "Add note...",
|
editorPlaceholderNote: "Add a note...",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { wrapText } from "../text.ts";
|
|||||||
import { UI_DIMENSIONS, UI_TEXT } from "./constants.ts";
|
import { UI_DIMENSIONS, UI_TEXT } from "./constants.ts";
|
||||||
|
|
||||||
type Theme = ExtensionContext["ui"]["theme"];
|
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_BORDER_PATTERN = /^[┌┐└┘─]+$/;
|
||||||
const EDITOR_SCROLL_BORDER_PATTERN = /^─── [↑↓] \d+ more ─*$/;
|
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[] {
|
function getEditorContentLines(editorLines: string[]): string[] {
|
||||||
if (editorLines.length <= 2) {
|
if (editorLines.length <= 2) {
|
||||||
return editorLines;
|
return editorLines;
|
||||||
|
|||||||
+49
-31
@@ -26,8 +26,10 @@ import type {
|
|||||||
export function renderQuestionScreen(context: QuestionRenderContext) {
|
export function renderQuestionScreen(context: QuestionRenderContext) {
|
||||||
const { lines, question, theme, width } = context;
|
const { lines, question, theme, width } = context;
|
||||||
pushWrappedText(lines, question.prompt, width, theme, "text", " ", " ");
|
pushWrappedText(lines, question.prompt, width, theme, "text", " ", " ");
|
||||||
lines.push("");
|
const renderedQuestionNote = renderQuestionNote(context);
|
||||||
renderQuestionNote(context);
|
if (!renderedQuestionNote) {
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
if (question.type === "preview") {
|
if (question.type === "preview") {
|
||||||
renderPreviewQuestion(context);
|
renderPreviewQuestion(context);
|
||||||
@@ -37,51 +39,35 @@ export function renderQuestionScreen(context: QuestionRenderContext) {
|
|||||||
renderStandardQuestion(context);
|
renderStandardQuestion(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderQuestionNote(context: QuestionRenderContext) {
|
function renderQuestionNote(context: QuestionRenderContext): boolean {
|
||||||
const { lines, state, question, theme, width, editor } = context;
|
const { lines, state, question, theme, width, editor } = context;
|
||||||
if (isQuestionNoteOpen(state, question.id)) {
|
if (isQuestionNoteOpen(state, question.id)) {
|
||||||
pushWrappedText(
|
renderQuestionNoteEditor(
|
||||||
lines,
|
lines,
|
||||||
UI_TEXT.questionNoteTitle,
|
editor,
|
||||||
width,
|
width,
|
||||||
theme,
|
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("");
|
lines.push("");
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingNote = getQuestionNote(state, question.id);
|
const existingNote = getQuestionNote(state, question.id);
|
||||||
if (!existingNote) {
|
if (!existingNote) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
pushWrappedText(
|
pushWrappedText(
|
||||||
lines,
|
lines,
|
||||||
`Question note: ${existingNote}`,
|
existingNote,
|
||||||
width,
|
width,
|
||||||
theme,
|
theme,
|
||||||
"muted",
|
"muted",
|
||||||
" ",
|
getSavedNotePrefix(theme, " "),
|
||||||
" "
|
getSavedNoteContinuationPrefix(" ")
|
||||||
);
|
);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStandardQuestion(context: QuestionRenderContext) {
|
function renderStandardQuestion(context: QuestionRenderContext) {
|
||||||
@@ -383,15 +369,39 @@ function renderSavedOptionNote(
|
|||||||
const { lines, theme, width } = context;
|
const { lines, theme, width } = context;
|
||||||
pushWrappedText(
|
pushWrappedText(
|
||||||
lines,
|
lines,
|
||||||
`Note: ${optionNote}`,
|
optionNote,
|
||||||
width,
|
width,
|
||||||
theme,
|
theme,
|
||||||
"muted",
|
"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(
|
function renderIndentedEditor(
|
||||||
lines: string[],
|
lines: string[],
|
||||||
editor: QuestionRenderContext["editor"],
|
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(
|
function getOptionPrefix(
|
||||||
questionType: QuestionRenderContext["question"]["type"],
|
questionType: QuestionRenderContext["question"]["type"],
|
||||||
option: AskDisplayOption,
|
option: AskDisplayOption,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { createInitialState } from "../src/state/create.ts";
|
import { createInitialState } from "../src/state/create.ts";
|
||||||
import { getRenderableOptions } from "../src/state/selectors.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";
|
import { renderQuestionScreen } from "../src/ui/render-question.ts";
|
||||||
|
|
||||||
function mockTheme() {
|
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...")));
|
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", () => {
|
test("selected multiline custom option renders full editor block", () => {
|
||||||
let state = createInitialState({
|
let state = createInitialState({
|
||||||
questions: [
|
questions: [
|
||||||
|
|||||||
Reference in New Issue
Block a user