pi-ask-popup 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -32
- package/docs/configuration.md +93 -0
- package/docs/hosts.md +71 -0
- package/docs/keyboard.md +65 -0
- package/docs/tool-schema.md +124 -0
- package/package.json +12 -2
- package/preview/popup-submit.webp +0 -0
- package/preview/popup-with-notes.webp +0 -0
- package/preview/popup-with-tab.webp +0 -0
- package/src/ask-user-question.ts +50 -24
- package/src/config.ts +66 -23
- package/src/rpc-fallback.ts +30 -15
- package/src/state/build-questionnaire.ts +3 -1
- package/src/state/external-editor.ts +6 -2
- package/src/state/key-router.ts +129 -41
- package/src/state/questionnaire-session.ts +35 -13
- package/src/state/row-intent.ts +6 -2
- package/src/state/selectors/derivations.ts +18 -6
- package/src/state/selectors/focus.ts +6 -2
- package/src/state/selectors/projections.ts +3 -1
- package/src/state/state-reducer.ts +99 -33
- package/src/tool/response-envelope.ts +32 -20
- package/src/tool/types.ts +13 -2
- package/src/view/component-binding.ts +3 -1
- package/src/view/components/inline-input.ts +3 -1
- package/src/view/components/multi-select-view.ts +15 -5
- package/src/view/components/preview/markdown-content-cache.ts +21 -6
- package/src/view/components/preview/preview-block-renderer.ts +8 -1
- package/src/view/components/preview/preview-box-renderer.ts +4 -5
- package/src/view/components/preview/preview-layout-decider.ts +20 -15
- package/src/view/components/preview/preview-pane.ts +54 -18
- package/src/view/components/wrapping-select.ts +15 -5
- package/src/view/dialog-builder.ts +24 -8
- package/src/view/props-adapter.ts +21 -7
- package/src/view/tab-content-strategy.ts +60 -20
|
@@ -119,17 +119,25 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
render(width: number): string[] {
|
|
122
|
-
if (this.question.multiSelect === true)
|
|
122
|
+
if (this.question.multiSelect === true) {
|
|
123
|
+
return this.optionListView.render(width);
|
|
124
|
+
}
|
|
123
125
|
// Spec: hide the preview pane entirely when no option carries a `preview`.
|
|
124
|
-
if (!this.previewBlock.hasAnyPreview())
|
|
126
|
+
if (!this.previewBlock.hasAnyPreview()) {
|
|
127
|
+
return this.optionListView.render(width);
|
|
128
|
+
}
|
|
125
129
|
// `inputMode` (typing on the "other" custom-answer row): the preview is irrelevant —
|
|
126
130
|
// the row sits at index `options.length`, out of bounds for any option's preview — so
|
|
127
131
|
// render the option list at the full pane width instead of the cramped left column.
|
|
128
132
|
// Side-by-side + preview block resume verbatim on nav-away (inputMode clears).
|
|
129
|
-
if (this.props.inputMode)
|
|
133
|
+
if (this.props.inputMode) {
|
|
134
|
+
return this.optionListView.render(width);
|
|
135
|
+
}
|
|
130
136
|
|
|
131
137
|
const mode = decideLayout(this.getTerminalWidth(), width);
|
|
132
|
-
if (mode === "side-by-side")
|
|
138
|
+
if (mode === "side-by-side") {
|
|
139
|
+
return this.renderSideBySide(width, mode);
|
|
140
|
+
}
|
|
133
141
|
|
|
134
142
|
// Stacked: options + blank gap + preview block.
|
|
135
143
|
return [
|
|
@@ -146,24 +154,38 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
|
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
focusedItemRowRange(width: number): [number, number] {
|
|
149
|
-
if (this.question.multiSelect === true)
|
|
150
|
-
|
|
157
|
+
if (this.question.multiSelect === true) {
|
|
158
|
+
return this.optionListView.focusedItemRowRange(width);
|
|
159
|
+
}
|
|
160
|
+
if (!this.previewBlock.hasAnyPreview()) {
|
|
161
|
+
return this.optionListView.focusedItemRowRange(width);
|
|
162
|
+
}
|
|
151
163
|
// `inputMode`: compute the focused row range against the FULL pane width (not the
|
|
152
164
|
// side-by-side leftWidth), mirroring `render`'s full-width option list.
|
|
153
|
-
if (this.props.inputMode)
|
|
165
|
+
if (this.props.inputMode) {
|
|
166
|
+
return this.optionListView.focusedItemRowRange(width);
|
|
167
|
+
}
|
|
154
168
|
const mode = decideLayout(this.getTerminalWidth(), width);
|
|
155
|
-
if (mode === "stacked")
|
|
169
|
+
if (mode === "stacked") {
|
|
170
|
+
return this.optionListView.focusedItemRowRange(width);
|
|
171
|
+
}
|
|
156
172
|
const adaptiveLeft = this.getAdaptiveLeft(width);
|
|
157
173
|
const { leftWidth } = columnWidths(width, adaptiveLeft);
|
|
158
174
|
return this.optionListView.focusedItemRowRange(leftWidth);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
naturalHeight(width: number): number {
|
|
162
|
-
if (this.question.multiSelect === true)
|
|
163
|
-
|
|
178
|
+
if (this.question.multiSelect === true) {
|
|
179
|
+
return this.optionListView.render(width).length;
|
|
180
|
+
}
|
|
181
|
+
if (!this.previewBlock.hasAnyPreview()) {
|
|
182
|
+
return this.optionListView.render(width).length;
|
|
183
|
+
}
|
|
164
184
|
// `inputMode`: height is the full-width option list only (no preview block) — preserves
|
|
165
185
|
// the `naturalHeight === render.length` parity invariant.
|
|
166
|
-
if (this.props.inputMode)
|
|
186
|
+
if (this.props.inputMode) {
|
|
187
|
+
return this.optionListView.render(width).length;
|
|
188
|
+
}
|
|
167
189
|
const mode = decideLayout(this.getTerminalWidth(), width);
|
|
168
190
|
const adaptiveLeft = this.getAdaptiveLeft(width);
|
|
169
191
|
const { optionsWidth, previewWidth } = bodyWidths(width, mode, adaptiveLeft);
|
|
@@ -173,16 +195,24 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
|
|
|
173
195
|
this.props.selectedIndex,
|
|
174
196
|
mode,
|
|
175
197
|
);
|
|
176
|
-
if (mode === "side-by-side")
|
|
198
|
+
if (mode === "side-by-side") {
|
|
199
|
+
return Math.max(optionsHeight, previewBlockHeight);
|
|
200
|
+
}
|
|
177
201
|
return optionsHeight + STACKED_GAP_ROWS + previewBlockHeight;
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
maxNaturalHeight(width: number): number {
|
|
181
|
-
if (this.question.multiSelect === true)
|
|
182
|
-
|
|
205
|
+
if (this.question.multiSelect === true) {
|
|
206
|
+
return this.optionListView.render(width).length;
|
|
207
|
+
}
|
|
208
|
+
if (!this.previewBlock.hasAnyPreview()) {
|
|
209
|
+
return this.optionListView.render(width).length;
|
|
210
|
+
}
|
|
183
211
|
// `inputMode`: like naturalHeight — full-width option list only, so the
|
|
184
212
|
// `maxNaturalHeight >= naturalHeight` parity invariant holds (both equal the list height).
|
|
185
|
-
if (this.props.inputMode)
|
|
213
|
+
if (this.props.inputMode) {
|
|
214
|
+
return this.optionListView.render(width).length;
|
|
215
|
+
}
|
|
186
216
|
const mode = decideLayout(this.getTerminalWidth(), width);
|
|
187
217
|
const adaptiveLeft = this.getAdaptiveLeft(width);
|
|
188
218
|
const { optionsWidth, previewWidth } = bodyWidths(width, mode, adaptiveLeft);
|
|
@@ -190,9 +220,13 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
|
|
|
190
220
|
let maxPreviewBlock = 0;
|
|
191
221
|
for (let i = 0; i < this.question.options.length; i++) {
|
|
192
222
|
const h = this.previewBlock.blockHeight(previewWidth, i, mode);
|
|
193
|
-
if (h > maxPreviewBlock)
|
|
223
|
+
if (h > maxPreviewBlock) {
|
|
224
|
+
maxPreviewBlock = h;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (mode === "side-by-side") {
|
|
228
|
+
return Math.max(optionsHeight, maxPreviewBlock);
|
|
194
229
|
}
|
|
195
|
-
if (mode === "side-by-side") return Math.max(optionsHeight, maxPreviewBlock);
|
|
196
230
|
return optionsHeight + STACKED_GAP_ROWS + maxPreviewBlock;
|
|
197
231
|
}
|
|
198
232
|
|
|
@@ -227,7 +261,9 @@ export class PreviewPane implements StatefulView<PreviewPaneProps>, Component {
|
|
|
227
261
|
const boxWidth = Math.max(1, visibleWidth(contentLines[0] ?? ""));
|
|
228
262
|
const boxAlignedPad = Math.max(PREVIEW_PADDING_LEFT, colWidth - boxWidth);
|
|
229
263
|
return contentLines.map((line) => {
|
|
230
|
-
if (line === "")
|
|
264
|
+
if (line === "") {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
231
267
|
// A line wider than the box (a long-locale notes affordance) slides left to
|
|
232
268
|
// stay fully visible; truncation engages only when the column itself runs out.
|
|
233
269
|
const pad = Math.max(
|
|
@@ -130,7 +130,9 @@ export class WrappingSelect implements Component {
|
|
|
130
130
|
invalidate(): void {}
|
|
131
131
|
|
|
132
132
|
render(width: number): string[] {
|
|
133
|
-
if (this.items.length === 0)
|
|
133
|
+
if (this.items.length === 0) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
134
136
|
|
|
135
137
|
const { startIndex, endIndex } = this.computeVisibleWindow();
|
|
136
138
|
const numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;
|
|
@@ -138,7 +140,9 @@ export class WrappingSelect implements Component {
|
|
|
138
140
|
|
|
139
141
|
for (let i = startIndex; i < endIndex; i++) {
|
|
140
142
|
const item = this.items[i];
|
|
141
|
-
if (!item)
|
|
143
|
+
if (!item) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
142
146
|
const isActive = i === this.selectedIndex && this.focused;
|
|
143
147
|
lines.push(...this.renderItem(item, i, isActive, width, numberWidth));
|
|
144
148
|
}
|
|
@@ -155,13 +159,17 @@ export class WrappingSelect implements Component {
|
|
|
155
159
|
* summing per-item row counts — O(maxVisible) per call.
|
|
156
160
|
*/
|
|
157
161
|
focusedItemRowRange(width: number): [number, number] {
|
|
158
|
-
if (this.items.length === 0)
|
|
162
|
+
if (this.items.length === 0) {
|
|
163
|
+
return [0, 0];
|
|
164
|
+
}
|
|
159
165
|
const { startIndex, endIndex } = this.computeVisibleWindow();
|
|
160
166
|
const numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;
|
|
161
167
|
let row = 0;
|
|
162
168
|
for (let i = startIndex; i < endIndex; i++) {
|
|
163
169
|
const item = this.items[i];
|
|
164
|
-
if (!item)
|
|
170
|
+
if (!item) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
165
173
|
const isActive = i === this.selectedIndex && this.focused;
|
|
166
174
|
const itemRowCount = this.computeItemRowCount(item, i, isActive, width, numberWidth);
|
|
167
175
|
if (i === this.selectedIndex) {
|
|
@@ -306,7 +314,9 @@ export class WrappingSelect implements Component {
|
|
|
306
314
|
continuationPrefix: string,
|
|
307
315
|
contentWidth: number,
|
|
308
316
|
): string[] {
|
|
309
|
-
if (!description)
|
|
317
|
+
if (!description) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
310
320
|
const wrapped = wrapTextWithAnsi(description, contentWidth);
|
|
311
321
|
return wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);
|
|
312
322
|
}
|
|
@@ -103,7 +103,9 @@ function computeScrollStart(
|
|
|
103
103
|
availableMiddle: number,
|
|
104
104
|
middleRows: number,
|
|
105
105
|
): number {
|
|
106
|
-
if (!bodyRange)
|
|
106
|
+
if (!bodyRange) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
107
109
|
const focusedRowInMiddle = headingCount + bodyRange[0];
|
|
108
110
|
const focusedHeight = bodyRange[1] - bodyRange[0];
|
|
109
111
|
const idealStart =
|
|
@@ -122,13 +124,19 @@ function decorateOverflow(
|
|
|
122
124
|
hasDown: boolean,
|
|
123
125
|
theme: Theme,
|
|
124
126
|
): void {
|
|
125
|
-
if (scrollableMiddle.length === 0)
|
|
127
|
+
if (scrollableMiddle.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
126
130
|
if (hasUp && hasDown && scrollableMiddle.length === 1) {
|
|
127
131
|
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_BOTH);
|
|
128
132
|
return;
|
|
129
133
|
}
|
|
130
|
-
if (hasUp)
|
|
131
|
-
|
|
134
|
+
if (hasUp) {
|
|
135
|
+
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
|
|
136
|
+
}
|
|
137
|
+
if (hasDown) {
|
|
138
|
+
scrollableMiddle[scrollableMiddle.length - 1] = theme.fg("dim", OVERFLOW_DOWN);
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
export type DialogState = QuestionnaireState;
|
|
@@ -307,16 +315,24 @@ export class DialogView implements StatefulView<DialogProps> {
|
|
|
307
315
|
const border = () => new DynamicBorder((s) => theme.fg("accent", s));
|
|
308
316
|
|
|
309
317
|
container.addChild(border());
|
|
310
|
-
if (isMulti && tabBar)
|
|
318
|
+
if (isMulti && tabBar) {
|
|
319
|
+
container.addChild(tabBar);
|
|
320
|
+
}
|
|
311
321
|
container.addChild(new Spacer(1));
|
|
312
322
|
|
|
313
|
-
for (const c of headingRowCache)
|
|
323
|
+
for (const c of headingRowCache) {
|
|
324
|
+
container.addChild(c);
|
|
325
|
+
}
|
|
314
326
|
container.addChild(strategy.bodyComponent(state));
|
|
315
327
|
container.addChild(new Spacer(1));
|
|
316
|
-
for (const c of strategy.midRows(state))
|
|
328
|
+
for (const c of strategy.midRows(state)) {
|
|
329
|
+
container.addChild(c);
|
|
330
|
+
}
|
|
317
331
|
|
|
318
332
|
container.addChild(border());
|
|
319
|
-
for (const c of strategy.footerRows(state))
|
|
333
|
+
for (const c of strategy.footerRows(state)) {
|
|
334
|
+
container.addChild(c);
|
|
335
|
+
}
|
|
320
336
|
|
|
321
337
|
// The residual spacer lives in render(), not here: whether it applies
|
|
322
338
|
// depends on overflow, which the container cannot see.
|
|
@@ -22,7 +22,9 @@ function getInputCursorOffset(input: Editor): number {
|
|
|
22
22
|
const lines = input.getLines();
|
|
23
23
|
const cursor = input.getCursor();
|
|
24
24
|
let offset = cursor.col;
|
|
25
|
-
for (let i = 0; i < cursor.line; i++)
|
|
25
|
+
for (let i = 0; i < cursor.line; i++) {
|
|
26
|
+
offset += (lines[i]?.length ?? 0) + 1;
|
|
27
|
+
}
|
|
26
28
|
return offset;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -82,7 +84,9 @@ export class QuestionnairePropsAdapter {
|
|
|
82
84
|
// so there is always a tab. Skipping the tick beats asserting non-null and
|
|
83
85
|
// throwing out of a render path if that ever stops being true.
|
|
84
86
|
const activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? firstTab?.preview;
|
|
85
|
-
if (!activePreviewPane)
|
|
87
|
+
if (!activePreviewPane) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
86
90
|
|
|
87
91
|
const ctx: BindingContext = {
|
|
88
92
|
questions: this.questions,
|
|
@@ -94,13 +98,19 @@ export class QuestionnairePropsAdapter {
|
|
|
94
98
|
activePreviewPane,
|
|
95
99
|
};
|
|
96
100
|
|
|
97
|
-
for (const binding of this.globalBindings)
|
|
101
|
+
for (const binding of this.globalBindings) {
|
|
102
|
+
binding.apply(state, ctx);
|
|
103
|
+
}
|
|
98
104
|
|
|
99
105
|
for (let i = 0; i < this.tabsByIndex.length; i++) {
|
|
100
106
|
const tab = this.tabsByIndex[i];
|
|
101
|
-
if (!tab)
|
|
107
|
+
if (!tab) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
102
110
|
const tabCtx: PerTabBindingContext = { ...ctx, tab, i };
|
|
103
|
-
for (const binding of this.perTabBindings)
|
|
111
|
+
for (const binding of this.perTabBindings) {
|
|
112
|
+
binding.apply(state, tabCtx);
|
|
113
|
+
}
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
this.tui.requestRender();
|
|
@@ -113,12 +123,16 @@ export class QuestionnairePropsAdapter {
|
|
|
113
123
|
* refresh them. Walks the same registries `apply()` does, then the extras.
|
|
114
124
|
*/
|
|
115
125
|
invalidate(): void {
|
|
116
|
-
for (const b of this.globalBindings)
|
|
126
|
+
for (const b of this.globalBindings) {
|
|
127
|
+
b.invalidate();
|
|
128
|
+
}
|
|
117
129
|
for (const tab of this.tabsByIndex) {
|
|
118
130
|
tab.optionList.invalidate();
|
|
119
131
|
tab.preview.invalidate();
|
|
120
132
|
tab.multiSelect?.invalidate();
|
|
121
133
|
}
|
|
122
|
-
for (const x of this.extraInvalidatables)
|
|
134
|
+
for (const x of this.extraInvalidatables) {
|
|
135
|
+
x.invalidate();
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
}
|
|
@@ -152,7 +152,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
152
152
|
bodyComponent(state: DialogState): Component {
|
|
153
153
|
const question = this.config.questions[state.currentTab];
|
|
154
154
|
const multiSelect = this.config.tabsByIndex[state.currentTab]?.multiSelect;
|
|
155
|
-
if (question?.multiSelect === true && multiSelect)
|
|
155
|
+
if (question?.multiSelect === true && multiSelect) {
|
|
156
|
+
return multiSelect;
|
|
157
|
+
}
|
|
156
158
|
return this.config.getPreviewPane();
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -161,7 +163,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
midRows(state: DialogState): Component[] {
|
|
164
|
-
if (!state.notesVisible)
|
|
166
|
+
if (!state.notesVisible) {
|
|
167
|
+
return this.restingNoteRows(state);
|
|
168
|
+
}
|
|
165
169
|
return [
|
|
166
170
|
new Text(this.config.theme.fg("muted", NOTES_HEADER), 1, 0),
|
|
167
171
|
this.config.notesInput,
|
|
@@ -190,9 +194,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
190
194
|
* put noise on every un-noted tab of every questionnaire.
|
|
191
195
|
*/
|
|
192
196
|
restingNoteRows(state: DialogState): Component[] {
|
|
193
|
-
if (this.restingNoteRowCount(state) === 0)
|
|
197
|
+
if (this.restingNoteRowCount(state) === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
194
200
|
const note = noteForTab(state, state.currentTab);
|
|
195
|
-
if (note.length === 0)
|
|
201
|
+
if (note.length === 0) {
|
|
202
|
+
return [new Spacer(1)];
|
|
203
|
+
}
|
|
196
204
|
return [
|
|
197
205
|
new OneLineClippedText(
|
|
198
206
|
this.config.theme.fg("dim", `${NOTES_LABEL} ${collapseToOneLine(note)}`),
|
|
@@ -213,9 +221,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
213
221
|
* then, and its own height is the intentional expansion.
|
|
214
222
|
*/
|
|
215
223
|
restingNoteRowCount(state: DialogState): number {
|
|
216
|
-
if (state.notesVisible)
|
|
224
|
+
if (state.notesVisible) {
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
217
227
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
218
|
-
if (noteForTab(state, i).length > 0)
|
|
228
|
+
if (noteForTab(state, i).length > 0) {
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
219
231
|
}
|
|
220
232
|
return 0;
|
|
221
233
|
}
|
|
@@ -273,13 +285,17 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
273
285
|
const c = new Container();
|
|
274
286
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
275
287
|
const q = this.config.questions[i];
|
|
276
|
-
if (!q)
|
|
288
|
+
if (!q) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
277
291
|
const a = state.answers.get(i);
|
|
278
292
|
const note = noteForTab(state, i);
|
|
279
293
|
// A note with no answer still gets an entry. Skipping it, which is what
|
|
280
294
|
// this loop used to do, dropped the note from the review and from the
|
|
281
295
|
// result: the user wrote something and was never told it went nowhere.
|
|
282
|
-
if (!a && note.length === 0)
|
|
296
|
+
if (!a && note.length === 0) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
283
299
|
c.addChild(new Text(this.config.theme.fg("muted", ` ● ${tabLabel(q.header, i)}`), 1, 0));
|
|
284
300
|
// No arrow row when there is no answer. The absent row is the signal, and
|
|
285
301
|
// the footer already names the question in its incomplete warning, so
|
|
@@ -326,7 +342,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
326
342
|
midRows(state: DialogState): Component[] {
|
|
327
343
|
// The question tabs' notes editor, mirrored for the global note. The draft
|
|
328
344
|
// itself is reducer-owned; this only renders it.
|
|
329
|
-
if (!state.notesVisible)
|
|
345
|
+
if (!state.notesVisible) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
330
348
|
return [
|
|
331
349
|
new Text(this.config.theme.fg("muted", GLOBAL_NOTES_HEADER), 1, 0),
|
|
332
350
|
this.config.notesInput,
|
|
@@ -338,7 +356,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
338
356
|
const missing: string[] = [];
|
|
339
357
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
340
358
|
const q = this.config.questions[i];
|
|
341
|
-
if (q && !state.answers.has(i))
|
|
359
|
+
if (q && !state.answers.has(i)) {
|
|
360
|
+
missing.push(tabLabel(q.header, i));
|
|
361
|
+
}
|
|
342
362
|
}
|
|
343
363
|
const promptText =
|
|
344
364
|
missing.length === 0
|
|
@@ -380,9 +400,13 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
380
400
|
}
|
|
381
401
|
|
|
382
402
|
function remainingLabel(state: DialogState): string | undefined {
|
|
383
|
-
if (state.timerCancelled)
|
|
403
|
+
if (state.timerCancelled) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
384
406
|
const ms = state.remainingMs;
|
|
385
|
-
if (ms === undefined)
|
|
407
|
+
if (ms === undefined) {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
386
410
|
const secs = Math.max(0, Math.ceil(ms / 1000));
|
|
387
411
|
return `${secs}s left`;
|
|
388
412
|
}
|
|
@@ -408,7 +432,9 @@ export function buildHintText(
|
|
|
408
432
|
collapseKey: string,
|
|
409
433
|
): string {
|
|
410
434
|
const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
|
|
411
|
-
if (question?.multiSelect === true)
|
|
435
|
+
if (question?.multiSelect === true) {
|
|
436
|
+
parts.push(HINT_PART_TOGGLE);
|
|
437
|
+
}
|
|
412
438
|
if (question && !state.notesVisible && !state.inputMode) {
|
|
413
439
|
// "add" is wrong once one exists, and the hint is the only affordance
|
|
414
440
|
// telling a keyboard user the resting row can be reopened at all.
|
|
@@ -416,17 +442,25 @@ export function buildHintText(
|
|
|
416
442
|
noteForTab(state, state.currentTab).length > 0 ? HINT_PART_NOTES_EDIT : HINT_PART_NOTES,
|
|
417
443
|
);
|
|
418
444
|
}
|
|
419
|
-
if (isMulti)
|
|
445
|
+
if (isMulti) {
|
|
446
|
+
parts.push(HINT_PART_TAB);
|
|
447
|
+
}
|
|
420
448
|
parts.push(HINT_PART_CANCEL);
|
|
421
449
|
if (collapseKey !== COLLAPSE_KEY_OFF) {
|
|
422
450
|
parts.push(
|
|
423
451
|
HINT_PART_COLLAPSE_TEMPLATE.replace(KEY_PLACEHOLDER, formatKeySpecForDisplay(collapseKey)),
|
|
424
452
|
);
|
|
425
453
|
}
|
|
426
|
-
if (state.notesVisible || state.inputMode)
|
|
427
|
-
|
|
454
|
+
if (state.notesVisible || state.inputMode) {
|
|
455
|
+
parts.push(HINT_PART_NEW_LINE);
|
|
456
|
+
}
|
|
457
|
+
if (state.inputMode) {
|
|
458
|
+
parts.push(HINT_PART_CLEAR);
|
|
459
|
+
}
|
|
428
460
|
const rem = remainingLabel(state);
|
|
429
|
-
if (rem)
|
|
461
|
+
if (rem) {
|
|
462
|
+
parts.push(rem);
|
|
463
|
+
}
|
|
430
464
|
return parts.join(" · ");
|
|
431
465
|
}
|
|
432
466
|
|
|
@@ -438,10 +472,16 @@ export function buildHintText(
|
|
|
438
472
|
*/
|
|
439
473
|
export function buildSubmitHintText(state: DialogState): string {
|
|
440
474
|
const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
|
|
441
|
-
if (!state.notesVisible)
|
|
475
|
+
if (!state.notesVisible) {
|
|
476
|
+
parts.push(REVIEW_GLOBAL_HINT);
|
|
477
|
+
}
|
|
442
478
|
parts.push(HINT_PART_CANCEL);
|
|
443
|
-
if (state.notesVisible)
|
|
479
|
+
if (state.notesVisible) {
|
|
480
|
+
parts.push(HINT_PART_NEW_LINE);
|
|
481
|
+
}
|
|
444
482
|
const rem = remainingLabel(state);
|
|
445
|
-
if (rem)
|
|
483
|
+
if (rem) {
|
|
484
|
+
parts.push(rem);
|
|
485
|
+
}
|
|
446
486
|
return parts.join(" · ");
|
|
447
487
|
}
|