revspec 0.2.1 → 0.3.0

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.
@@ -1,87 +1,139 @@
1
- import { TextRenderable, type CliRenderer } from "@opentui/core";
1
+ import { BoxRenderable, TextRenderable, TextNodeRenderable, TextAttributes, type CliRenderer } from "@opentui/core";
2
2
  import type { ReviewState } from "../state/review-state";
3
3
  import { basename } from "path";
4
4
  import { theme } from "./theme";
5
5
 
6
6
  export interface TopBarComponents {
7
- bar: TextRenderable;
7
+ box: BoxRenderable;
8
+ text: TextRenderable;
8
9
  }
9
10
 
10
11
  export interface BottomBarComponents {
11
- bar: TextRenderable;
12
+ box: BoxRenderable;
13
+ text: TextRenderable;
12
14
  }
13
15
 
14
16
  /**
15
- * Build the top bar text: filename + thread summary.
17
+ * Build the top bar with styled TextNodes.
16
18
  */
17
- export function buildTopBarText(
19
+ export function buildTopBar(
20
+ bar: TopBarComponents,
18
21
  specFile: string,
19
22
  state: ReviewState,
20
23
  unreadCount?: number,
21
24
  specChanged?: boolean,
22
- mode?: "markdown" | "line"
23
- ): string {
25
+ ): void {
26
+ const t = bar.text;
27
+ t.clear();
24
28
  const name = basename(specFile);
25
- const modeLabel = mode === "markdown" ? "[md]" : mode === "line" ? "[line]" : "";
26
29
  const { open, pending } = state.activeThreadCount();
27
- const parts: string[] = [];
28
- if (open > 0) parts.push(`${open} open`);
29
- if (pending > 0) parts.push(`${pending} pending`);
30
- const threadSummary =
31
- parts.length > 0 ? `Threads: ${parts.join(", ")}` : "No active threads";
32
- let result = ` ${name} ${modeLabel} | ${threadSummary}`;
30
+
31
+ // Filename bold
32
+ t.add(TextNodeRenderable.fromString(` ${name}`, { fg: theme.text, attributes: TextAttributes.BOLD }));
33
+
34
+ t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
35
+
36
+ // Thread summary
37
+ if (open > 0 || pending > 0) {
38
+ const parts: string[] = [];
39
+ if (open > 0) parts.push(`${open} open`);
40
+ if (pending > 0) parts.push(`${pending} pending`);
41
+ t.add(TextNodeRenderable.fromString(parts.join(", "), { fg: theme.yellow }));
42
+ } else {
43
+ t.add(TextNodeRenderable.fromString("No active threads", { fg: theme.subtext }));
44
+ }
45
+
46
+ // Unread replies
33
47
  if (unreadCount && unreadCount > 0) {
34
- result += ` | ${unreadCount} new repl${unreadCount === 1 ? "y" : "ies"}`;
48
+ t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
49
+ t.add(TextNodeRenderable.fromString(
50
+ `${unreadCount} new repl${unreadCount === 1 ? "y" : "ies"}`,
51
+ { fg: theme.green, attributes: TextAttributes.BOLD }
52
+ ));
35
53
  }
54
+
55
+ // Spec changed warning
36
56
  if (specChanged) {
37
- result += ` | !! Spec changed externally`;
57
+ t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
58
+ t.add(TextNodeRenderable.fromString("!! Spec changed externally", { fg: theme.red, attributes: TextAttributes.BOLD }));
38
59
  }
39
- result += ` | L${state.cursorLine}/${state.lineCount}`;
40
- return result;
60
+
61
+ // Cursor position
62
+ t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
63
+ t.add(TextNodeRenderable.fromString(`L${state.cursorLine}/${state.lineCount}`, { fg: theme.subtext }));
41
64
  }
42
65
 
43
66
  /**
44
- * Build the bottom bar text: keybinding hints.
45
- * Contextually shows command buffer when in command mode.
46
- * Prepends mode indicator when provided.
67
+ * Build the bottom bar with styled TextNodes.
47
68
  */
48
- export function buildBottomBarText(commandBuffer: string | null): string {
69
+ export function buildBottomBar(bar: BottomBarComponents, commandBuffer: string | null): void {
70
+ const t = bar.text;
71
+ t.clear();
49
72
  if (commandBuffer !== null) {
50
- return ` :${commandBuffer}`;
73
+ t.add(TextNodeRenderable.fromString(` :${commandBuffer}`, { fg: theme.text }));
74
+ return;
75
+ }
76
+ const hints = [
77
+ { key: "j/k", action: "move" },
78
+ { key: "c", action: "comment" },
79
+ { key: "r", action: "resolve" },
80
+ { key: "/", action: "search" },
81
+ { key: "?", action: "help" },
82
+ ];
83
+ t.add(TextNodeRenderable.fromString(" ", {}));
84
+ for (let i = 0; i < hints.length; i++) {
85
+ const h = hints[i];
86
+ t.add(TextNodeRenderable.fromString(`[${h.key}]`, { fg: theme.blue }));
87
+ t.add(TextNodeRenderable.fromString(` ${h.action}`, { fg: theme.subtext }));
88
+ if (i < hints.length - 1) {
89
+ t.add(TextNodeRenderable.fromString(" ", {}));
90
+ }
51
91
  }
52
- return ` [j/k] move [c] comment [r] resolve [/] search [?] help`;
53
92
  }
54
93
 
55
94
  /**
56
- * Create the top status bar.
95
+ * Create the top status bar (BoxRenderable with backgroundColor for full-width fill).
57
96
  */
58
97
  export function createTopBar(renderer: CliRenderer): TopBarComponents {
59
- const bar = new TextRenderable(renderer, {
60
- content: "",
98
+ const box = new BoxRenderable(renderer, {
61
99
  width: "100%",
62
100
  height: 1,
63
- bg: theme.surface0,
101
+ backgroundColor: theme.base,
102
+ border: ["bottom"],
103
+ borderColor: theme.surface1,
104
+ });
105
+
106
+ const text = new TextRenderable(renderer, {
107
+ content: "",
108
+ width: "100%",
64
109
  fg: theme.text,
65
110
  wrapMode: "none",
66
111
  truncate: true,
67
112
  });
68
113
 
69
- return { bar };
114
+ box.add(text);
115
+ return { box, text };
70
116
  }
71
117
 
72
118
  /**
73
119
  * Create the bottom status bar.
74
120
  */
75
121
  export function createBottomBar(renderer: CliRenderer): BottomBarComponents {
76
- const bar = new TextRenderable(renderer, {
77
- content: "",
122
+ const box = new BoxRenderable(renderer, {
78
123
  width: "100%",
79
124
  height: 1,
80
- bg: theme.surface0,
125
+ flexShrink: 0,
126
+ backgroundColor: theme.surface0,
127
+ });
128
+
129
+ const text = new TextRenderable(renderer, {
130
+ content: "",
131
+ width: "100%",
81
132
  fg: theme.text,
82
133
  wrapMode: "none",
83
134
  truncate: true,
84
135
  });
85
136
 
86
- return { bar };
137
+ box.add(text);
138
+ return { box, text };
87
139
  }