@tenyokjs/devdevian-cli 0.7.0-alpha.1 → 0.7.2-alpha.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 +8 -2
- package/VERSIONS.md +14 -0
- package/bin/devdevian.mjs +1 -0
- package/lib/conversation-links.mjs +13 -0
- package/lib/form-guidance.mjs +38 -0
- package/lib/tui-data.mjs +4 -2
- package/lib/tui.mjs +193 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ The default `devdevian` command and `devdevian tui` open an alternate terminal s
|
|
|
38
38
|
- **N** creates a conversation (a community on the Communities screen); **A** asks a question; **R** replies inside a conversation; **Shift+R** refreshes.
|
|
39
39
|
- **C** comments on snippet lines; **E/D** edit/delete your own ordinary replies or code feedback; **S** resolves/reopens a code remark or accepts/removes a reply as the solution (question/review author only); **!** reports selected code feedback.
|
|
40
40
|
- **Ctrl+E** opens the current multiline field in `$VISUAL` / `$EDITOR` (use `code --wait` for VS Code). Closing the editor restores the form; publishing still requires preview and confirmation. **Ctrl+L** saves an active draft and returns to sign-in.
|
|
41
|
-
- In forms, **Tab/Shift+Tab** change fields, arrows choose options or move the text cursor, **Enter** adds a line, **Ctrl+S** opens the preview. Publication requires a separate confirmation; Escape asks before discarding a draft. Drafts auto-save locally after a short pause and can be restored from **Local drafts** or `/drafts`.
|
|
41
|
+
- In forms, **Tab/Shift+Tab** change fields or focus the step controls, arrows choose options or move the text cursor, **Enter** adds a line inside multiline text, and **Ctrl+S** opens the preview. Publication requires a separate confirmation; Escape asks before discarding a draft. Drafts auto-save locally after a short pause and can be restored from **Local drafts** or `/drafts`.
|
|
42
42
|
- **Ctrl+C** exits. With an unsent draft, confirm saving and leaving or press Ctrl+C again to force exit.
|
|
43
43
|
|
|
44
44
|
`devdevian open` launches Terminal on macOS, Windows Terminal on Windows, or GNOME Terminal/Konsole/xterm on a Linux desktop. A graphical desktop and an installed supported terminal are required; over SSH and inside an editor, use `devdevian tui`. Window launchers still need platform-specific user testing; terminal rendering and input are covered by macOS/Linux pseudo-terminal checks.
|
|
@@ -63,10 +63,16 @@ Forms autosave locally. Successful upload IDs are retained if validation fails;
|
|
|
63
63
|
|
|
64
64
|
Press **Ctrl+K** outside a form or command input, or run **/actions**, to search actions, sections and creation flows. Recent views stay in memory for the current account/session; the palette offers up to eight. Type `project 7`, `event 9` or `open 142` for an explicit ID, or choose the forum-search result for a text query. Typing stays local; actions run only after Enter. Destructive actions retain their separate confirmation. **Esc** returns to the previous view without moving your selection.
|
|
65
65
|
|
|
66
|
-
Conversation, Project and Event forms show one step at a time. **Ctrl+N** validates the current step and moves forward; **Ctrl+P** returns to the previous step. **Tab**
|
|
66
|
+
Conversation, Project and Event forms show one step at a time. **Ctrl+N** validates the current step and moves forward; **Ctrl+P** returns to the previous step. **Tab / Shift+Tab** move through the current step and its **Back / Next** controls. Use arrows to choose a control and **Enter** to activate it; the last step offers **Preview**, not immediate publication. **Ctrl+S** checks every required field and opens the full publication preview; a missing value returns you to its step. Step changes preserve text, image paths, code selections and locally saved drafts. No upload or publication happens just by advancing a step.
|
|
67
67
|
|
|
68
68
|
In full-text reading (**Space** on a reply or remark), **/** opens local text search. **Enter** locates a matching rendered line; **n / Shift+N** cycles forward/backward. **Home / End** jumps to the start/end. **Esc** returns to the underlying view, and reopening the same unchanged item restores your place. This is bounded session memory, not cross-device or permanent reading history. In text fields and the command bar, **Ctrl+K** continues to delete to the end of the line.
|
|
69
69
|
|
|
70
|
+
Active fields explain their input: Space stays a space, Enter adds a line in multiline fields, and Tab leaves the field. Project feedback accepts text and URLs; dedicated website/repository/additional links are in **People & links**. Additional links use one `Label | https://example.com` per line. Context hints stay below the editor on larger terminals and use the status/hint lines on compact ones. **Ctrl+S** checks the complete form and opens a preview; drafts autosave locally and publication still requires explicit confirmation.
|
|
71
|
+
|
|
72
|
+
Multiline fields show the current logical line and total line count. While editing, the command bar gives its space to the form; the caret stays visible as you add lines or move through the text. Previous lines remain part of the same field, including after Enter. On short terminals only a window of a long message is visible; use ↑/↓ to revisit earlier lines or Ctrl+E for the external editor.
|
|
73
|
+
|
|
74
|
+
Conversation overviews and publication previews display Demo/Repository URL fields alongside any labeled links, with duplicate destinations shown once.
|
|
75
|
+
|
|
70
76
|
## Meet Devi
|
|
71
77
|
|
|
72
78
|
Devi is the Devdevian mascot: a small ghost with a laptop and an orbit. The terminal uses colored ASCII art, so it needs no image protocol or image downloads. A compact drawing is used when space is limited; authentication codes, addresses and controls take priority over decoration.
|
package/VERSIONS.md
CHANGED
|
@@ -8,6 +8,20 @@ Every published npm artifact has a unique immutable version. We record each vers
|
|
|
8
8
|
|
|
9
9
|
**0.7.0 focuses on everyday terminal use**: discoverable quick actions, staged forms and resumable reading.
|
|
10
10
|
|
|
11
|
+
## 0.7.2-alpha.1 — Visible links and multiline writing
|
|
12
|
+
|
|
13
|
+
- Conversation overviews now include Demo and Repository URL fields alongside labeled links. Duplicate URLs appear once, preserving a supplied label; publication previews show the same destinations.
|
|
14
|
+
- Multiline editing follows the caret and displays the logical line position/count. Enter retains the preceding visible line when space permits. While editing, the inactive command bar yields its rows to the form, including at 60×18.
|
|
15
|
+
- Real terminal/API regressions cover saved Demo URLs together with labeled links, newline visibility, cursor movement and editing in compact terminals. No API/schema or credential changes.
|
|
16
|
+
|
|
17
|
+
## 0.7.1-alpha.1 — Clearer forms
|
|
18
|
+
|
|
19
|
+
- Added visible, keyboard-focusable Back / Next controls to staged forms. Tab reaches the controls at step boundaries, arrows choose an available action and Enter activates it. The final action is Preview; publication remains a separate confirmation. Ctrl+N/P shortcuts remain available.
|
|
20
|
+
- Pinned input guidance distinguishes Space, multiline Enter, field navigation and image/code pickers. Project help explicitly points to the dedicated People & links step. Compact layouts prioritize the caret and keep guidance in status/hint lines.
|
|
21
|
+
- Clarified additional-link syntax, preview versus autosaved drafts, and optional URL fields. Opening a new form clears stale success notices.
|
|
22
|
+
- Regression coverage publishes a project through the real terminal and isolated API, checks text spaces/newlines and multiple labeled links from a second account, exercises Back/Next/Preview, preserves Git/image selections and checks multiline editing at 60×18.
|
|
23
|
+
- No API/schema change, new dependency or production-data reset. Per-version history is recorded here; no separate GitHub release is required for this patch.
|
|
24
|
+
|
|
11
25
|
## 0.7.0-alpha.1 — Everyday flow
|
|
12
26
|
|
|
13
27
|
- Added a searchable quick-action workspace via `Ctrl+K` or `/actions`: scoped actions, navigation, creation, up to eight recent views, explicit ID opening and a regular forum-search fallback. Typing performs no requests or mutations; existing server permissions and confirmation dialogs still apply.
|
package/bin/devdevian.mjs
CHANGED
|
@@ -117,6 +117,7 @@ const help = `Devdevian CLI ${version} · alpha
|
|
|
117
117
|
Inside the workspace: /projects, /project ID, /events, /event ID
|
|
118
118
|
Ctrl+K or /actions opens quick actions; type to filter
|
|
119
119
|
Ctrl+N / Ctrl+P moves between form steps; Ctrl+S validates and previews
|
|
120
|
+
Forms: Tab fields/buttons, Enter selects Back/Next or adds a line in multiline text
|
|
120
121
|
Full reply text: / find, n/N matches, Home/End, Esc remembers your place
|
|
121
122
|
N creates · F filters · E edits your publication · X opens context actions
|
|
122
123
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Keep legacy URL fields visible alongside labeled links, without repeating a URL.
|
|
2
|
+
export function conversationLinks(post = {}) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
return [
|
|
5
|
+
...(post.links || []),
|
|
6
|
+
...(post.demo_url ? [{ label: "Demo", url: post.demo_url }] : []),
|
|
7
|
+
...(post.repo_url ? [{ label: "Repository", url: post.repo_url }] : []),
|
|
8
|
+
].filter((link) => {
|
|
9
|
+
if (!link.url || seen.has(link.url)) return false;
|
|
10
|
+
seen.add(link.url);
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function fieldGuidance(field, { wizard = false, project = false } = {}) {
|
|
2
|
+
if (!field)
|
|
3
|
+
return {
|
|
4
|
+
detail: "",
|
|
5
|
+
keys: wizard
|
|
6
|
+
? "Enter selects the highlighted action"
|
|
7
|
+
: "Enter opens the preview",
|
|
8
|
+
};
|
|
9
|
+
let detail = field.hint || "";
|
|
10
|
+
if (project && field.key === "help")
|
|
11
|
+
detail =
|
|
12
|
+
"Text and links are allowed. Dedicated links: People & links step.";
|
|
13
|
+
if (["url", "repo", "demo_url"].includes(field.key) && !detail)
|
|
14
|
+
detail = "Use a complete https:// or http:// URL.";
|
|
15
|
+
if (["logo", "banner"].includes(field.key))
|
|
16
|
+
return { detail, keys: "Ctrl+O selects an image · Tab next field" };
|
|
17
|
+
if (field.key === "file")
|
|
18
|
+
return { detail, keys: "Ctrl+O file · Ctrl+G Git diff · Tab next field" };
|
|
19
|
+
const keys = field.options
|
|
20
|
+
? "← / → choose · Tab next field"
|
|
21
|
+
: field.multiline
|
|
22
|
+
? "Space inserts a space · Enter adds a line · Tab next field"
|
|
23
|
+
: "Space inserts a space · Enter / Tab next field";
|
|
24
|
+
return { detail, keys };
|
|
25
|
+
}
|
|
26
|
+
export function stepControls(steps, index) {
|
|
27
|
+
return [
|
|
28
|
+
{ id: "back", label: "← Back", enabled: index > 0 },
|
|
29
|
+
{
|
|
30
|
+
id: "next",
|
|
31
|
+
label:
|
|
32
|
+
index === steps.length - 1
|
|
33
|
+
? "Preview →"
|
|
34
|
+
: "Next: " + steps[index + 1]?.label + " →",
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
package/lib/tui-data.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadCatalog, isCatalog } from "./catalog.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { conversationLinks } from "./conversation-links.mjs";
|
|
3
4
|
export function assertSnippet(code, hash) {
|
|
4
5
|
if (
|
|
5
6
|
createHash("sha256")
|
|
@@ -288,11 +289,12 @@ export function contentLines(route, data) {
|
|
|
288
289
|
.split("\n")
|
|
289
290
|
.map((line, index) => String(index + 1).padStart(4) + " " + line),
|
|
290
291
|
);
|
|
291
|
-
|
|
292
|
+
const links = conversationLinks(p);
|
|
293
|
+
if (links.length)
|
|
292
294
|
lines.push(
|
|
293
295
|
"",
|
|
294
296
|
"LINKS",
|
|
295
|
-
...
|
|
297
|
+
...links.map((link) => link.label + " · " + link.url),
|
|
296
298
|
);
|
|
297
299
|
if (p.archived_at || p.locked_at)
|
|
298
300
|
lines.push("", "Replies are paused for this conversation.");
|
package/lib/tui.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
appearanceFlags,
|
|
44
44
|
} from "./appearance.mjs";
|
|
45
45
|
import { liveLoop, anchoredIndex } from "./live.mjs";
|
|
46
|
+
import { fieldGuidance, stepControls } from "./form-guidance.mjs";
|
|
46
47
|
import { paletteItems } from "./palette.mjs";
|
|
47
48
|
import {
|
|
48
49
|
formSteps,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
import { contextActions } from "./context-actions.mjs";
|
|
55
56
|
import { editExternal } from "./external-editor.mjs";
|
|
56
57
|
import { markdownRows } from "./rich-text.mjs";
|
|
58
|
+
import { conversationLinks } from "./conversation-links.mjs";
|
|
57
59
|
import { problemMessage } from "./problems.mjs";
|
|
58
60
|
import { ReviewWorkspace, codeRange } from "./review-workspace.mjs";
|
|
59
61
|
import {
|
|
@@ -289,21 +291,34 @@ function InputText({ value, cursor, width, height = 1 }) {
|
|
|
289
291
|
}),
|
|
290
292
|
);
|
|
291
293
|
}
|
|
292
|
-
function FormView({ form, active, cursor, width, height }) {
|
|
294
|
+
function FormView({ form, active, cursor, width, height, navFocus }) {
|
|
293
295
|
const colors = useTheme();
|
|
294
296
|
const fields = fieldsFor(form),
|
|
295
297
|
rows = [];
|
|
296
298
|
const steps = formSteps(form, fields),
|
|
297
299
|
stepIndex = stepAt(steps, active, fields.length),
|
|
298
300
|
step = steps[stepIndex];
|
|
301
|
+
const guidance = fieldGuidance(navFocus ? null : fields[active], {
|
|
302
|
+
wizard: !!step,
|
|
303
|
+
project: form.kind === "catalog" && form.entity === "project",
|
|
304
|
+
});
|
|
305
|
+
const helpRows =
|
|
306
|
+
form.kind === "appearance" ? 0 : height >= 8 ? 2 : height >= 5 ? 1 : 0,
|
|
307
|
+
navRows = step && height >= 3 ? 1 : 0;
|
|
308
|
+
const editorHeight = Math.max(1, height - helpRows - navRows);
|
|
299
309
|
let selectedLine = rows.length;
|
|
310
|
+
let caretRow = null;
|
|
300
311
|
fields.forEach((field, index) => {
|
|
301
312
|
if (step && !step.indexes.includes(index)) return;
|
|
302
313
|
if (index === active) selectedLine = rows.length;
|
|
314
|
+
const value = form.values[field.key] || "";
|
|
315
|
+
const lineNumber = field.multiline
|
|
316
|
+
? graphemes(value).slice(0, cursor).join("").split("\n").length
|
|
317
|
+
: 0;
|
|
303
318
|
rows.push({
|
|
304
|
-
text: (index === active ? "› " : " ") + field.label,
|
|
305
|
-
color: index === active ? colors.purple : colors.muted,
|
|
306
|
-
bold: index === active,
|
|
319
|
+
text: (index === active && !navFocus ? "› " : " ") + field.label,
|
|
320
|
+
color: index === active && !navFocus ? colors.purple : colors.muted,
|
|
321
|
+
bold: index === active && !navFocus,
|
|
307
322
|
});
|
|
308
323
|
if (field.options) {
|
|
309
324
|
const option = field.options.find(
|
|
@@ -312,12 +327,11 @@ function FormView({ form, active, cursor, width, height }) {
|
|
|
312
327
|
rows.push({
|
|
313
328
|
text:
|
|
314
329
|
" ‹ " + (option?.[1] || form.values[field.key] || "Choose") + " ›",
|
|
315
|
-
color: index === active ? colors.blue : colors.text,
|
|
330
|
+
color: index === active && !navFocus ? colors.blue : colors.text,
|
|
316
331
|
});
|
|
317
332
|
} else {
|
|
318
|
-
const value = form.values[field.key] || "";
|
|
319
333
|
const text =
|
|
320
|
-
index === active
|
|
334
|
+
index === active && !navFocus
|
|
321
335
|
? cursorLines(value, cursor, width - 3).lines
|
|
322
336
|
: wrap(value || "…", width - 3);
|
|
323
337
|
const caretLine =
|
|
@@ -329,24 +343,30 @@ function FormView({ form, active, cursor, width, height }) {
|
|
|
329
343
|
: 0;
|
|
330
344
|
const visible = field.multiline
|
|
331
345
|
? Math.min(
|
|
332
|
-
index === active
|
|
346
|
+
index === active
|
|
347
|
+
? Math.max(1, Math.min(12, editorHeight - 1))
|
|
348
|
+
: 3,
|
|
333
349
|
Math.max(2, text.length),
|
|
334
350
|
)
|
|
335
351
|
: 1,
|
|
336
352
|
start = index === active ? Math.max(0, caretLine - visible + 1) : 0;
|
|
353
|
+
if (index === active && !navFocus && field.multiline) {
|
|
354
|
+
rows.at(-1).text += ` · line ${lineNumber}/${value.split("\n").length}`;
|
|
355
|
+
// Follow the caret, rather than anchoring the viewport to the field label.
|
|
356
|
+
// A newline keeps its preceding row visible whenever the terminal has room.
|
|
357
|
+
caretRow = rows.length + caretLine - start;
|
|
358
|
+
}
|
|
337
359
|
rows.push(
|
|
338
360
|
...text.slice(start, start + visible).map((line) => ({
|
|
339
361
|
text: " " + line,
|
|
340
|
-
color: index === active ? colors.text : colors.muted,
|
|
362
|
+
color: index === active && !navFocus ? colors.text : colors.muted,
|
|
341
363
|
})),
|
|
342
364
|
);
|
|
343
365
|
}
|
|
344
|
-
if (field.hint && active === index)
|
|
345
|
-
rows.push({ text: " " + field.hint, color: colors.muted });
|
|
346
366
|
rows.push("");
|
|
347
367
|
});
|
|
348
368
|
if (active === fields.length) selectedLine = rows.length;
|
|
349
|
-
if (!step
|
|
369
|
+
if (!step)
|
|
350
370
|
rows.push({
|
|
351
371
|
text:
|
|
352
372
|
(active === fields.length ? "› " : " ") +
|
|
@@ -360,11 +380,60 @@ function FormView({ form, active, cursor, width, height }) {
|
|
|
360
380
|
color: colors.blue,
|
|
361
381
|
bold: true,
|
|
362
382
|
});
|
|
363
|
-
|
|
383
|
+
let offset = Math.max(
|
|
364
384
|
0,
|
|
365
|
-
Math.min(
|
|
385
|
+
Math.min(
|
|
386
|
+
selectedLine - (editorHeight >= 4 ? 1 : 0),
|
|
387
|
+
rows.length - editorHeight,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
if (caretRow !== null && caretRow >= offset + editorHeight)
|
|
391
|
+
offset = caretRow - editorHeight + 1;
|
|
392
|
+
return h(
|
|
393
|
+
Box,
|
|
394
|
+
{ flexDirection: "column", height, overflow: "hidden" },
|
|
395
|
+
h(FrameRows, { rows, width, height: editorHeight, offset }),
|
|
396
|
+
helpRows === 2
|
|
397
|
+
? h(TextLine, {
|
|
398
|
+
text:
|
|
399
|
+
guidance.detail ||
|
|
400
|
+
(form.kind === "catalogFilter"
|
|
401
|
+
? "Ctrl+S applies these filters."
|
|
402
|
+
: form.kind === "calendar"
|
|
403
|
+
? "Ctrl+S exports a new calendar file."
|
|
404
|
+
: "Ctrl+S opens a preview; publishing requires separate confirmation."),
|
|
405
|
+
width,
|
|
406
|
+
color: colors.muted,
|
|
407
|
+
})
|
|
408
|
+
: null,
|
|
409
|
+
helpRows
|
|
410
|
+
? h(TextLine, { text: guidance.keys, width, color: colors.muted })
|
|
411
|
+
: null,
|
|
412
|
+
navRows
|
|
413
|
+
? h(
|
|
414
|
+
Text,
|
|
415
|
+
{ wrap: "truncate" },
|
|
416
|
+
...stepControls(steps, stepIndex).map((control) =>
|
|
417
|
+
h(
|
|
418
|
+
Text,
|
|
419
|
+
{
|
|
420
|
+
key: control.id,
|
|
421
|
+
color: !control.enabled
|
|
422
|
+
? colors.muted
|
|
423
|
+
: navFocus === control.id
|
|
424
|
+
? colors.blue
|
|
425
|
+
: colors.purple,
|
|
426
|
+
bold: navFocus === control.id,
|
|
427
|
+
dimColor: !control.enabled,
|
|
428
|
+
backgroundColor:
|
|
429
|
+
navFocus === control.id ? colors.selected : undefined,
|
|
430
|
+
},
|
|
431
|
+
` ${navFocus === control.id ? "›" : " "}[ ${control.label} ] `,
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
: null,
|
|
366
436
|
);
|
|
367
|
-
return h(FrameRows, { rows, width, height, offset });
|
|
368
437
|
}
|
|
369
438
|
function listRows(route, data, index, width, colors, density = "comfortable") {
|
|
370
439
|
const items = data.items || [];
|
|
@@ -498,12 +567,11 @@ function documentRows(route, data, width, colors) {
|
|
|
498
567
|
...markdownRows(fence + "\n" + post.code + "\n" + fence, width, colors),
|
|
499
568
|
);
|
|
500
569
|
}
|
|
501
|
-
|
|
570
|
+
const links = conversationLinks(post);
|
|
571
|
+
if (links.length)
|
|
502
572
|
rows.push(
|
|
503
573
|
{ text: "LINKS", bold: true },
|
|
504
|
-
...
|
|
505
|
-
wrap(link.label + " · " + link.url, width),
|
|
506
|
-
),
|
|
574
|
+
...links.flatMap((link) => wrap(link.label + " · " + link.url, width)),
|
|
507
575
|
);
|
|
508
576
|
if (post.archived_at || post.locked_at)
|
|
509
577
|
rows.push({
|
|
@@ -591,6 +659,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
591
659
|
[notice, setNotice] = useState(initialAppearance.warning || "");
|
|
592
660
|
const [form, setForm] = useState(null),
|
|
593
661
|
[field, setField] = useState(0),
|
|
662
|
+
[formNav, setFormNav] = useState(null),
|
|
594
663
|
[cursor, setCursor] = useState(0),
|
|
595
664
|
[preview, setPreview] = useState(null),
|
|
596
665
|
[dialog, setDialog] = useState(null),
|
|
@@ -651,7 +720,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
651
720
|
height = Math.max(1, rows - 1),
|
|
652
721
|
compact = width < 90 || effectiveAppearance.sidebar === "hidden",
|
|
653
722
|
sidebar = compact ? 0 : 25,
|
|
654
|
-
|
|
723
|
+
editingForm = !!form && !preview && !picker,
|
|
724
|
+
bodyHeight = Math.max(1, height - (editingForm ? 6 : 9)),
|
|
655
725
|
contentWidth = Math.max(1, width - sidebar - 6),
|
|
656
726
|
contentHeight = Math.max(1, bodyHeight - 6);
|
|
657
727
|
const key = JSON.stringify(route),
|
|
@@ -893,6 +963,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
893
963
|
setReaderFind({ query: "", cursor: 0, editing: false });
|
|
894
964
|
}
|
|
895
965
|
function moveStep(direction) {
|
|
966
|
+
setFormNav(null);
|
|
896
967
|
const fields = fieldsFor(form),
|
|
897
968
|
steps = formSteps(form, fields),
|
|
898
969
|
currentStep = stepAt(steps, field, fields.length);
|
|
@@ -1392,6 +1463,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1392
1463
|
);
|
|
1393
1464
|
}
|
|
1394
1465
|
function openForm(next) {
|
|
1466
|
+
setFormNav(null);
|
|
1467
|
+
setNotice("");
|
|
1395
1468
|
setBooting(false);
|
|
1396
1469
|
setForm({
|
|
1397
1470
|
...next,
|
|
@@ -1556,6 +1629,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1556
1629
|
}
|
|
1557
1630
|
async function prepare() {
|
|
1558
1631
|
if (!form) return;
|
|
1632
|
+
setFormNav(null);
|
|
1559
1633
|
if (form.kind === "catalogFilter") {
|
|
1560
1634
|
await task(async () => {
|
|
1561
1635
|
const values = validateFilter(form.values);
|
|
@@ -2128,7 +2202,19 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2128
2202
|
}
|
|
2129
2203
|
function changeField(next) {
|
|
2130
2204
|
const fields = fieldsFor(form),
|
|
2131
|
-
|
|
2205
|
+
steps = formSteps(form, fields),
|
|
2206
|
+
stepIndex = stepAt(steps, field, fields.length),
|
|
2207
|
+
step = steps[stepIndex];
|
|
2208
|
+
if (
|
|
2209
|
+
step &&
|
|
2210
|
+
!formNav &&
|
|
2211
|
+
((next > field && field === step.indexes.at(-1)) ||
|
|
2212
|
+
(next < field && field === step.indexes[0]))
|
|
2213
|
+
) {
|
|
2214
|
+
setFormNav(next < field && stepIndex > 0 ? "back" : "next");
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const selected = (next + fields.length + 1) % (fields.length + 1);
|
|
2132
2218
|
setField(selected);
|
|
2133
2219
|
setCursor(
|
|
2134
2220
|
selected < fields.length
|
|
@@ -2324,7 +2410,12 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2324
2410
|
return;
|
|
2325
2411
|
}
|
|
2326
2412
|
if (form) {
|
|
2327
|
-
if (
|
|
2413
|
+
if (
|
|
2414
|
+
keyEvent.ctrl &&
|
|
2415
|
+
input === "o" &&
|
|
2416
|
+
form.kind === "catalog" &&
|
|
2417
|
+
!formNav
|
|
2418
|
+
) {
|
|
2328
2419
|
const active = fieldsFor(form)[field];
|
|
2329
2420
|
if (["logo", "banner", "screenshots"].includes(active?.key)) {
|
|
2330
2421
|
setPicker({ mediaKey: active.key });
|
|
@@ -2359,7 +2450,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2359
2450
|
moveStep(input === "n" ? 1 : -1);
|
|
2360
2451
|
return;
|
|
2361
2452
|
}
|
|
2362
|
-
if (keyEvent.ctrl && input === "e") {
|
|
2453
|
+
if (keyEvent.ctrl && input === "e" && !formNav) {
|
|
2363
2454
|
externalEdit();
|
|
2364
2455
|
return;
|
|
2365
2456
|
}
|
|
@@ -2367,6 +2458,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2367
2458
|
keyEvent.ctrl &&
|
|
2368
2459
|
["o", "g"].includes(input) &&
|
|
2369
2460
|
form.kind === "publication" &&
|
|
2461
|
+
!formNav &&
|
|
2370
2462
|
form.values.kind === "review"
|
|
2371
2463
|
) {
|
|
2372
2464
|
setPicker(input === "g" ? "changes" : "files");
|
|
@@ -2390,6 +2482,39 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2390
2482
|
prepare();
|
|
2391
2483
|
return;
|
|
2392
2484
|
}
|
|
2485
|
+
if (formNav) {
|
|
2486
|
+
const fields = fieldsFor(form),
|
|
2487
|
+
steps = formSteps(form, fields),
|
|
2488
|
+
i = stepAt(steps, field, fields.length),
|
|
2489
|
+
step = steps[i];
|
|
2490
|
+
if (keyEvent.return) {
|
|
2491
|
+
moveStep(formNav === "back" ? -1 : 1);
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
if (keyEvent.leftArrow || keyEvent.upArrow) {
|
|
2495
|
+
setFormNav(i > 0 ? "back" : "next");
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
if (keyEvent.rightArrow || keyEvent.downArrow) {
|
|
2499
|
+
setFormNav("next");
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (keyEvent.tab) {
|
|
2503
|
+
if (keyEvent.shift && formNav === "next" && i > 0) {
|
|
2504
|
+
setFormNav("back");
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (!keyEvent.shift && formNav === "back") {
|
|
2508
|
+
setFormNav("next");
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
const next = keyEvent.shift ? step.indexes.at(-1) : step.indexes[0];
|
|
2512
|
+
setFormNav(null);
|
|
2513
|
+
setField(next);
|
|
2514
|
+
setCursor(graphemes(form.values[fields[next].key] || "").length);
|
|
2515
|
+
}
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2393
2518
|
if (keyEvent.tab) {
|
|
2394
2519
|
changeField(field + (keyEvent.shift ? -1 : 1));
|
|
2395
2520
|
return;
|
|
@@ -2735,8 +2860,16 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2735
2860
|
reportSelected();
|
|
2736
2861
|
}
|
|
2737
2862
|
});
|
|
2863
|
+
const compactFieldHelp =
|
|
2864
|
+
form && !formNav && form.kind !== "appearance" && contentHeight < 8
|
|
2865
|
+
? fieldGuidance(fieldsFor(form)[field], {
|
|
2866
|
+
wizard: !!activeSteps.length,
|
|
2867
|
+
project: form.kind === "catalog" && form.entity === "project",
|
|
2868
|
+
}).detail
|
|
2869
|
+
: "";
|
|
2738
2870
|
const status =
|
|
2739
2871
|
error ||
|
|
2872
|
+
compactFieldHelp ||
|
|
2740
2873
|
liveStatus ||
|
|
2741
2874
|
notice ||
|
|
2742
2875
|
(pending
|
|
@@ -3030,6 +3163,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
3030
3163
|
? [payload.title || payload.name, ""]
|
|
3031
3164
|
: []),
|
|
3032
3165
|
payload.body || payload.reason || payload.description || "",
|
|
3166
|
+
...conversationLinks(payload).flatMap((link) => [
|
|
3167
|
+
"",
|
|
3168
|
+
link.label + " · " + link.url,
|
|
3169
|
+
]),
|
|
3033
3170
|
...(preview.catalog
|
|
3034
3171
|
? [
|
|
3035
3172
|
"",
|
|
@@ -3087,6 +3224,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
3087
3224
|
body = h(form.kind === "appearance" ? AppearanceView : FormView, {
|
|
3088
3225
|
form,
|
|
3089
3226
|
active: field,
|
|
3227
|
+
navFocus: formNav,
|
|
3090
3228
|
cursor,
|
|
3091
3229
|
width: contentWidth,
|
|
3092
3230
|
height: contentHeight,
|
|
@@ -3432,7 +3570,11 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
3432
3570
|
: preview
|
|
3433
3571
|
? "← → choose · Enter confirm · Esc edit"
|
|
3434
3572
|
: form
|
|
3435
|
-
?
|
|
3573
|
+
? formNav
|
|
3574
|
+
? `${stepControls(activeSteps, activeStep).find((control) => control.id === formNav)?.label || "Next"} · Enter select · Tab fields · Ctrl+S preview`
|
|
3575
|
+
: fieldsFor(form)[field]?.multiline
|
|
3576
|
+
? "Enter new line · Tab next field · Ctrl+N next step · Ctrl+S preview"
|
|
3577
|
+
: "Enter/Tab next field · Ctrl+N/P steps · Ctrl+S preview"
|
|
3436
3578
|
: phase !== "ready"
|
|
3437
3579
|
? "Enter connect / open browser · Ctrl+C exit"
|
|
3438
3580
|
: route.view === "feedback"
|
|
@@ -3489,31 +3631,33 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
3489
3631
|
width,
|
|
3490
3632
|
color: error ? colors.red : notice ? colors.green : colors.muted,
|
|
3491
3633
|
}),
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3634
|
+
editingForm
|
|
3635
|
+
? null
|
|
3636
|
+
: h(
|
|
3637
|
+
Box,
|
|
3638
|
+
{
|
|
3639
|
+
height: 3,
|
|
3640
|
+
borderStyle: colors.frame,
|
|
3641
|
+
borderColor: focus === "prompt" ? colors.blue : colors.line,
|
|
3642
|
+
paddingX: 1,
|
|
3643
|
+
overflow: "hidden",
|
|
3644
|
+
},
|
|
3645
|
+
h(Text, { color: colors.purple, bold: true }, "› "),
|
|
3646
|
+
focus === "prompt"
|
|
3647
|
+
? h(InputText, {
|
|
3648
|
+
value: command,
|
|
3649
|
+
cursor: commandCursor,
|
|
3650
|
+
width: Math.max(1, width - 6),
|
|
3651
|
+
})
|
|
3652
|
+
: h(
|
|
3653
|
+
Text,
|
|
3654
|
+
{ color: colors.muted },
|
|
3655
|
+
clip(
|
|
3656
|
+
"Search or /command · Press / to type · X actions · , appearance",
|
|
3657
|
+
width - 6,
|
|
3658
|
+
),
|
|
3659
|
+
),
|
|
3660
|
+
),
|
|
3517
3661
|
h(TextLine, { text: hint, width, color: colors.muted }),
|
|
3518
3662
|
),
|
|
3519
3663
|
);
|