dual-brain 0.2.6 → 0.2.8

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/src/tui.mjs CHANGED
@@ -171,6 +171,85 @@ export function menu(options, opts = {}) {
171
171
  return rows.join('\n');
172
172
  }
173
173
 
174
+ // ── Modern box rendering with rounded corners ────────────────────────────────
175
+
176
+ const ROUNDED = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', ml: '├', mr: '┤' };
177
+
178
+ export function panel(title, content, opts = {}) {
179
+ const { width = 70, titleColor = '\x1b[36m', borderColor = '\x1b[2m', reset = '\x1b[0m' } = opts;
180
+ const lines = [];
181
+ const innerW = width - 2;
182
+
183
+ // Top border with title
184
+ if (title) {
185
+ const titleStr = ` ${title} `;
186
+ const remaining = innerW - titleStr.length - 1;
187
+ lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h} ${titleColor}${title}${borderColor} ${ROUNDED.h.repeat(Math.max(0, remaining))}${ROUNDED.tr}${reset}`);
188
+ } else {
189
+ lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h.repeat(innerW)}${ROUNDED.tr}${reset}`);
190
+ }
191
+
192
+ // Content lines
193
+ const contentLines = (typeof content === 'string' ? content.split('\n') : content);
194
+ for (const line of contentLines) {
195
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
196
+ const pad = Math.max(0, innerW - stripped.length);
197
+ lines.push(`${borderColor}${ROUNDED.v}${reset} ${line}${' '.repeat(pad)}${borderColor}${ROUNDED.v}${reset}`);
198
+ }
199
+
200
+ // Bottom border
201
+ lines.push(`${borderColor}${ROUNDED.bl}${ROUNDED.h.repeat(innerW)}${ROUNDED.br}${reset}`);
202
+
203
+ return lines.join('\n');
204
+ }
205
+
206
+ export function divider(width = 70) {
207
+ const borderColor = '\x1b[2m';
208
+ const reset = '\x1b[0m';
209
+ return `${borderColor}${ROUNDED.ml}${ROUNDED.h.repeat(width - 2)}${ROUNDED.mr}${reset}`;
210
+ }
211
+
212
+ export function statusChip(label, healthy, opts = {}) {
213
+ const green = '\x1b[32m';
214
+ const red = '\x1b[31m';
215
+ const dim = '\x1b[2m';
216
+ const reset = '\x1b[0m';
217
+ const icon = healthy ? `${green}●${reset}` : `${red}●${reset}`;
218
+ return `${icon} ${dim}${label}${reset}`;
219
+ }
220
+
221
+ export function headerBar(left, right, width = 70) {
222
+ const leftStripped = left.replace(/\x1b\[[0-9;]*m/g, '');
223
+ const rightStripped = right.replace(/\x1b\[[0-9;]*m/g, '');
224
+ const gap = Math.max(1, width - leftStripped.length - rightStripped.length);
225
+ return `${left}${' '.repeat(gap)}${right}`;
226
+ }
227
+
228
+ export function prompt(text = '> task or /help') {
229
+ const cyan = '\x1b[36m';
230
+ const dim = '\x1b[2m';
231
+ const reset = '\x1b[0m';
232
+ return `${cyan}>${reset} ${dim}${text.replace(/^>\s*/, '')}${reset}`;
233
+ }
234
+
235
+ export function signalLine(type, text, meta = '') {
236
+ const green = '\x1b[32m';
237
+ const yellow = '\x1b[33m';
238
+ const dim = '\x1b[2m';
239
+ const reset = '\x1b[0m';
240
+
241
+ let icon;
242
+ switch (type) {
243
+ case 'success': icon = `${green}✓${reset}`; break;
244
+ case 'warning': icon = `${yellow}!${reset}`; break;
245
+ case 'info': icon = `${dim}·${reset}`; break;
246
+ default: icon = `${dim}·${reset}`;
247
+ }
248
+
249
+ const metaStr = meta ? `${dim}${meta}${reset}` : '';
250
+ return `${icon} ${text}${metaStr ? ' ' + metaStr : ''}`;
251
+ }
252
+
174
253
  // ─── Self-test ────────────────────────────────────────────────────────────────
175
254
 
176
255
  if (process.argv[1] === fileURLToPath(import.meta.url)) {