@kaban-board/tui 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +1958 -0
  2. package/package.json +50 -0
package/dist/index.js ADDED
@@ -0,0 +1,1958 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
+
21
+ // src/index.ts
22
+ import { BoardService as BoardService2, createDb as createDb2, TaskService } from "@kaban-board/core";
23
+ import { createCliRenderer } from "@opentui/core";
24
+
25
+ // src/components/board.ts
26
+ import { BoxRenderable as BoxRenderable2, fg, SelectRenderable, TextRenderable as TextRenderable2, t } from "@opentui/core";
27
+
28
+ // src/lib/theme.ts
29
+ var LOGO = "\uD83D\uDC17";
30
+ var COLORS = {
31
+ bg: "#0d1117",
32
+ panel: "#161b22",
33
+ border: "#30363d",
34
+ borderActive: "#58a6ff",
35
+ text: "#e6edf3",
36
+ textMuted: "#8b949e",
37
+ textDim: "#6e7681",
38
+ accent: "#58a6ff",
39
+ accentBright: "#79c0ff",
40
+ danger: "#f85149",
41
+ success: "#3fb950",
42
+ warning: "#d29922",
43
+ overlay: "#00000088",
44
+ inputBg: "#21262d",
45
+ cursor: "#79c0ff"
46
+ };
47
+ var STATUS_COLORS = {
48
+ backlog: COLORS.textDim,
49
+ todo: COLORS.warning,
50
+ "in-progress": COLORS.accent,
51
+ done: COLORS.success
52
+ };
53
+ function getStatusColor(columnId) {
54
+ return STATUS_COLORS[columnId] ?? COLORS.textMuted;
55
+ }
56
+
57
+ // src/lib/utils.ts
58
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
59
+ function truncate(str, maxLength) {
60
+ if (str.length <= maxLength)
61
+ return str;
62
+ return `${str.slice(0, maxLength - 1)}…`;
63
+ }
64
+ function truncateMiddle(str, maxLength) {
65
+ if (str.length <= maxLength)
66
+ return str;
67
+ if (maxLength <= 3)
68
+ return str.slice(0, maxLength);
69
+ const ellipsis = "...";
70
+ const charsToShow = maxLength - ellipsis.length;
71
+ const frontChars = Math.ceil(charsToShow / 2);
72
+ const backChars = Math.floor(charsToShow / 2);
73
+ return str.slice(0, frontChars) + ellipsis + str.slice(-backChars);
74
+ }
75
+ function formatRelativeTime(date) {
76
+ if (!date)
77
+ return "";
78
+ const now = new Date;
79
+ const diffMs = now.getTime() - date.getTime();
80
+ const diffSec = Math.floor(diffMs / 1000);
81
+ const diffMin = Math.floor(diffSec / 60);
82
+ const diffHour = Math.floor(diffMin / 60);
83
+ const diffDay = Math.floor(diffHour / 24);
84
+ if (diffSec < 60)
85
+ return "just now";
86
+ if (diffMin < 60)
87
+ return `${diffMin} min ago`;
88
+ if (diffHour < 24)
89
+ return `${diffHour}h ago`;
90
+ if (diffDay < 7)
91
+ return `${diffDay}d ago`;
92
+ if (diffDay < 30)
93
+ return `${Math.floor(diffDay / 7)}w ago`;
94
+ return `${Math.floor(diffDay / 30)}mo ago`;
95
+ }
96
+ function createSectionDivider(renderer, options) {
97
+ const { label, width, id } = options;
98
+ const labelWithPadding = `─ ${label} `;
99
+ const remainingWidth = width - labelWithPadding.length;
100
+ const line = "─".repeat(Math.max(0, remainingWidth));
101
+ const row = new BoxRenderable(renderer, {
102
+ id: `${id}-divider`,
103
+ width: "100%",
104
+ height: 1
105
+ });
106
+ const dividerText = new TextRenderable(renderer, {
107
+ id: `${id}-divider-text`,
108
+ content: labelWithPadding + line,
109
+ fg: COLORS.border
110
+ });
111
+ row.add(dividerText);
112
+ return row;
113
+ }
114
+
115
+ // src/components/board.ts
116
+ async function refreshBoard(state) {
117
+ const { renderer, taskService, boardService } = state;
118
+ if (state.mainContainer) {
119
+ state.mainContainer.destroy();
120
+ }
121
+ state.columns = await boardService.getColumns();
122
+ state.taskSelects = new Map;
123
+ const tasks = await taskService.listTasks();
124
+ const mainContainer = new BoxRenderable2(renderer, {
125
+ id: "main",
126
+ width: "100%",
127
+ height: "100%",
128
+ flexDirection: "column",
129
+ backgroundColor: COLORS.bg
130
+ });
131
+ state.mainContainer = mainContainer;
132
+ const header = new BoxRenderable2(renderer, {
133
+ id: "header",
134
+ width: "100%",
135
+ height: 3,
136
+ backgroundColor: COLORS.panel,
137
+ border: true,
138
+ borderColor: COLORS.border,
139
+ justifyContent: "center",
140
+ alignItems: "center"
141
+ });
142
+ const headerText = new TextRenderable2(renderer, {
143
+ id: "header-text",
144
+ content: t`${fg(COLORS.warning)(LOGO)} ${fg(COLORS.accent)(state.boardName)}`
145
+ });
146
+ header.add(headerText);
147
+ mainContainer.add(header);
148
+ const columnsContainer = new BoxRenderable2(renderer, {
149
+ id: "columns-container",
150
+ flexDirection: "row",
151
+ width: "100%",
152
+ flexGrow: 1,
153
+ gap: 1,
154
+ padding: 1
155
+ });
156
+ state.columnPanels = [];
157
+ for (let i = 0;i < state.columns.length; i++) {
158
+ const column = state.columns[i];
159
+ const columnTasks = tasks.filter((t2) => t2.columnId === column.id);
160
+ const isSelected = i === state.currentColumnIndex;
161
+ const columnPanel = new BoxRenderable2(renderer, {
162
+ id: `column-${column.id}`,
163
+ flexGrow: 1,
164
+ flexDirection: "column",
165
+ border: true,
166
+ borderStyle: isSelected ? "double" : "single",
167
+ borderColor: isSelected ? COLORS.borderActive : COLORS.border,
168
+ backgroundColor: COLORS.panel,
169
+ padding: 1
170
+ });
171
+ const columnHeader = new BoxRenderable2(renderer, {
172
+ id: `column-header-${column.id}`,
173
+ width: "100%",
174
+ height: 1,
175
+ justifyContent: "center"
176
+ });
177
+ const columnTitle = new TextRenderable2(renderer, {
178
+ id: `column-title-${column.id}`,
179
+ content: `${column.name} (${columnTasks.length})`,
180
+ fg: isSelected ? COLORS.accentBright : COLORS.textMuted
181
+ });
182
+ columnHeader.add(columnTitle);
183
+ columnPanel.add(columnHeader);
184
+ const taskContainer = new BoxRenderable2(renderer, {
185
+ id: `task-container-${column.id}`,
186
+ width: "100%",
187
+ flexGrow: 1,
188
+ flexDirection: "column"
189
+ });
190
+ if (columnTasks.length > 0) {
191
+ const taskSelect = new SelectRenderable(renderer, {
192
+ id: `tasks-${column.id}`,
193
+ width: "100%",
194
+ height: "100%",
195
+ backgroundColor: COLORS.panel,
196
+ textColor: COLORS.text,
197
+ options: columnTasks.map((task) => ({
198
+ name: truncate(task.title, 30),
199
+ description: task.assignedTo ?? task.createdBy,
200
+ value: task.id
201
+ })),
202
+ selectedBackgroundColor: COLORS.bg,
203
+ selectedTextColor: COLORS.accent,
204
+ descriptionColor: COLORS.textMuted
205
+ });
206
+ state.taskSelects.set(column.id, taskSelect);
207
+ if (isSelected) {
208
+ taskSelect.focus();
209
+ }
210
+ taskContainer.add(taskSelect);
211
+ } else {
212
+ const emptyText = new TextRenderable2(renderer, {
213
+ id: `empty-${column.id}`,
214
+ content: "(empty)",
215
+ fg: COLORS.textDim
216
+ });
217
+ taskContainer.add(emptyText);
218
+ }
219
+ columnPanel.add(taskContainer);
220
+ state.columnPanels.push(columnPanel);
221
+ columnsContainer.add(columnPanel);
222
+ }
223
+ mainContainer.add(columnsContainer);
224
+ const footer = new BoxRenderable2(renderer, {
225
+ id: "footer",
226
+ width: "100%",
227
+ height: 3,
228
+ backgroundColor: COLORS.panel,
229
+ border: true,
230
+ borderColor: COLORS.border,
231
+ justifyContent: "center",
232
+ alignItems: "center"
233
+ });
234
+ const footerText = new TextRenderable2(renderer, {
235
+ id: "footer-text",
236
+ content: "[a]dd [m]ove [u]ser [d]el [?]Help [q]uit",
237
+ fg: COLORS.textMuted
238
+ });
239
+ footer.add(footerText);
240
+ mainContainer.add(footer);
241
+ renderer.root.add(mainContainer);
242
+ }
243
+
244
+ // src/components/modals/add-task.ts
245
+ import {
246
+ BoxRenderable as BoxRenderable5,
247
+ InputRenderable,
248
+ InputRenderableEvents,
249
+ TextRenderable as TextRenderable4
250
+ } from "@opentui/core";
251
+
252
+ // src/lib/button-row.ts
253
+ import { BoxRenderable as BoxRenderable3, TextRenderable as TextRenderable3 } from "@opentui/core";
254
+ function createButtonRow(renderer, id, buttons) {
255
+ const container = new BoxRenderable3(renderer, {
256
+ id: `${id}-button-row`,
257
+ width: "100%",
258
+ height: 1,
259
+ flexDirection: "row",
260
+ justifyContent: "center",
261
+ gap: 2
262
+ });
263
+ const buttonTexts = [];
264
+ const state = {
265
+ container,
266
+ selectedIndex: 0,
267
+ focused: false,
268
+ busy: false,
269
+ buttons,
270
+ setFocused: (focused) => {
271
+ state.focused = focused;
272
+ state.render();
273
+ },
274
+ selectNext: () => {
275
+ if (!state.focused)
276
+ return;
277
+ state.selectedIndex = (state.selectedIndex + 1) % buttons.length;
278
+ state.render();
279
+ },
280
+ selectPrev: () => {
281
+ if (!state.focused)
282
+ return;
283
+ state.selectedIndex = (state.selectedIndex - 1 + buttons.length) % buttons.length;
284
+ state.render();
285
+ },
286
+ triggerSelected: async () => {
287
+ if (!state.focused)
288
+ return;
289
+ if (state.busy)
290
+ return;
291
+ state.busy = true;
292
+ try {
293
+ await buttons[state.selectedIndex].action();
294
+ } finally {
295
+ state.busy = false;
296
+ }
297
+ },
298
+ render: () => {
299
+ for (let i = 0;i < buttons.length; i++) {
300
+ const btn = buttons[i];
301
+ const isSelected = state.focused && i === state.selectedIndex;
302
+ buttonTexts[i].content = isSelected ? `[${btn.label}]` : ` ${btn.label} `;
303
+ buttonTexts[i].fg = isSelected ? btn.color ?? COLORS.accentBright : COLORS.textMuted;
304
+ buttonTexts[i].bg = isSelected ? COLORS.inputBg : undefined;
305
+ }
306
+ }
307
+ };
308
+ for (let i = 0;i < buttons.length; i++) {
309
+ const btn = buttons[i];
310
+ const isSelected = state.focused && i === state.selectedIndex;
311
+ const text = new TextRenderable3(renderer, {
312
+ id: `${id}-btn-${i}`,
313
+ content: isSelected ? `[${btn.label}]` : ` ${btn.label} `,
314
+ fg: isSelected ? btn.color ?? COLORS.accentBright : COLORS.textMuted,
315
+ bg: isSelected ? COLORS.inputBg : undefined
316
+ });
317
+ buttonTexts.push(text);
318
+ container.add(text);
319
+ }
320
+ return state;
321
+ }
322
+
323
+ // src/components/overlay.ts
324
+ import { BoxRenderable as BoxRenderable4 } from "@opentui/core";
325
+ function createModalOverlay(renderer, options) {
326
+ const overlay = new BoxRenderable4(renderer, {
327
+ id: "modal-overlay",
328
+ width: "100%",
329
+ height: "100%",
330
+ position: "absolute",
331
+ top: 0,
332
+ left: 0,
333
+ backgroundColor: COLORS.overlay,
334
+ justifyContent: "center",
335
+ alignItems: "center"
336
+ });
337
+ const dialog = new BoxRenderable4(renderer, {
338
+ id: options.id,
339
+ width: options.width,
340
+ height: options.height,
341
+ flexDirection: "column",
342
+ border: true,
343
+ borderStyle: "double",
344
+ borderColor: options.borderColor ?? COLORS.accent,
345
+ backgroundColor: COLORS.panel,
346
+ padding: options.padding ?? 1,
347
+ justifyContent: options.justifyContent,
348
+ alignItems: options.alignItems
349
+ });
350
+ overlay.add(dialog);
351
+ return { overlay, dialog };
352
+ }
353
+
354
+ // src/components/modals/shared.ts
355
+ function closeModal(state) {
356
+ if (state.viewTaskRuntime?.copyTimeoutId) {
357
+ clearTimeout(state.viewTaskRuntime.copyTimeoutId);
358
+ }
359
+ if (state.modalOverlay) {
360
+ state.modalOverlay.destroy();
361
+ state.modalOverlay = null;
362
+ }
363
+ state.taskInput = null;
364
+ state.buttonRow = null;
365
+ state.selectedTask = null;
366
+ state.onModalConfirm = null;
367
+ state.viewTaskState = null;
368
+ state.editTaskState = null;
369
+ state.viewTaskRuntime = null;
370
+ state.editTaskRuntime = null;
371
+ state.activeModal = "none";
372
+ refocusCurrentColumnSelect(state);
373
+ }
374
+ function refocusCurrentColumnSelect(state) {
375
+ const column = state.columns[state.currentColumnIndex];
376
+ if (!column)
377
+ return;
378
+ const select = state.taskSelects.get(column.id);
379
+ if (select) {
380
+ select.focus();
381
+ }
382
+ }
383
+ function blurCurrentColumnSelect(state) {
384
+ const column = state.columns[state.currentColumnIndex];
385
+ if (!column)
386
+ return;
387
+ const select = state.taskSelects.get(column.id);
388
+ if (select) {
389
+ select.blur();
390
+ }
391
+ }
392
+
393
+ // src/components/modals/add-task.ts
394
+ function showAddTaskModal(state, onTaskCreated) {
395
+ const { renderer, columns, currentColumnIndex } = state;
396
+ const column = columns[currentColumnIndex];
397
+ if (!column)
398
+ return;
399
+ const { overlay, dialog } = createModalOverlay(renderer, {
400
+ id: "add-task-dialog",
401
+ width: 52,
402
+ height: 11
403
+ });
404
+ const titleRow = new BoxRenderable5(renderer, {
405
+ id: "title-row",
406
+ width: "100%",
407
+ height: 1
408
+ });
409
+ const title = new TextRenderable4(renderer, {
410
+ id: "dialog-title",
411
+ content: `Add task to "${column.name}"`,
412
+ fg: COLORS.accent
413
+ });
414
+ titleRow.add(title);
415
+ const spacer1 = new BoxRenderable5(renderer, { id: "dialog-spacer1", width: "100%", height: 1 });
416
+ const labelRow = new BoxRenderable5(renderer, {
417
+ id: "label-row",
418
+ width: "100%",
419
+ height: 1
420
+ });
421
+ const label = new TextRenderable4(renderer, {
422
+ id: "dialog-label",
423
+ content: "Task title:",
424
+ fg: COLORS.text
425
+ });
426
+ labelRow.add(label);
427
+ const input = new InputRenderable(renderer, {
428
+ id: "task-title-input",
429
+ width: 46,
430
+ height: 1,
431
+ placeholder: "Enter task title...",
432
+ textColor: COLORS.text,
433
+ placeholderColor: COLORS.textDim,
434
+ backgroundColor: COLORS.inputBg,
435
+ focusedBackgroundColor: COLORS.inputBg,
436
+ cursorColor: COLORS.cursor
437
+ });
438
+ const spacer2 = new BoxRenderable5(renderer, { id: "dialog-spacer2", width: "100%", height: 1 });
439
+ const doCreate = async () => {
440
+ const taskTitle = input.value.trim();
441
+ if (taskTitle) {
442
+ await state.taskService.addTask({ title: taskTitle, columnId: column.id });
443
+ }
444
+ closeModal(state);
445
+ await onTaskCreated();
446
+ };
447
+ const doCancel = () => {
448
+ closeModal(state);
449
+ };
450
+ const buttonRow = createButtonRow(renderer, "add-task", [
451
+ { label: "Create", action: doCreate, color: COLORS.success },
452
+ { label: "Cancel", action: doCancel }
453
+ ]);
454
+ dialog.add(titleRow);
455
+ dialog.add(spacer1);
456
+ dialog.add(labelRow);
457
+ dialog.add(input);
458
+ dialog.add(spacer2);
459
+ dialog.add(buttonRow.container);
460
+ renderer.root.add(overlay);
461
+ setImmediate(() => {
462
+ buttonRow.setFocused(false);
463
+ input.focus();
464
+ });
465
+ state.modalOverlay = overlay;
466
+ state.taskInput = input;
467
+ state.buttonRow = buttonRow;
468
+ state.activeModal = "addTask";
469
+ input.on(InputRenderableEvents.ENTER, doCreate);
470
+ }
471
+ // src/components/modals/assign-task.ts
472
+ import {
473
+ BoxRenderable as BoxRenderable6,
474
+ InputRenderable as InputRenderable2,
475
+ InputRenderableEvents as InputRenderableEvents2,
476
+ TextRenderable as TextRenderable5
477
+ } from "@opentui/core";
478
+
479
+ // src/lib/types.ts
480
+ function getSelectedTaskId(state) {
481
+ const column = state.columns[state.currentColumnIndex];
482
+ if (!column)
483
+ return null;
484
+ const select = state.taskSelects.get(column.id);
485
+ if (!select)
486
+ return null;
487
+ const selected = select.getSelectedOption();
488
+ const value = selected?.value;
489
+ return typeof value === "string" ? value : null;
490
+ }
491
+
492
+ // src/components/modals/assign-task.ts
493
+ async function showAssignTaskModal(state, onAssigned) {
494
+ const { renderer } = state;
495
+ const taskId = getSelectedTaskId(state);
496
+ if (!taskId) {
497
+ return;
498
+ }
499
+ const task = await state.taskService.getTask(taskId);
500
+ if (!task) {
501
+ return;
502
+ }
503
+ const { overlay, dialog } = createModalOverlay(renderer, {
504
+ id: "assign-task-dialog",
505
+ width: 45,
506
+ height: 12
507
+ });
508
+ const titleRow = new BoxRenderable6(renderer, {
509
+ id: "assign-title-row",
510
+ width: "100%",
511
+ height: 1
512
+ });
513
+ const title = new TextRenderable5(renderer, {
514
+ id: "assign-title",
515
+ content: "Assign Task",
516
+ fg: COLORS.accent
517
+ });
518
+ titleRow.add(title);
519
+ const taskRow = new BoxRenderable6(renderer, {
520
+ id: "assign-task-row",
521
+ width: "100%",
522
+ height: 1
523
+ });
524
+ const taskText = new TextRenderable5(renderer, {
525
+ id: "assign-task-text",
526
+ content: task.title.slice(0, 40),
527
+ fg: COLORS.textMuted
528
+ });
529
+ taskRow.add(taskText);
530
+ const spacer1 = new BoxRenderable6(renderer, { id: "assign-spacer1", width: "100%", height: 1 });
531
+ const labelRow = new BoxRenderable6(renderer, {
532
+ id: "assign-label-row",
533
+ width: "100%",
534
+ height: 1
535
+ });
536
+ const currentAssignee = task.assignedTo ?? "(unassigned)";
537
+ const label = new TextRenderable5(renderer, {
538
+ id: "assign-label",
539
+ content: `Current: ${currentAssignee}`,
540
+ fg: COLORS.text
541
+ });
542
+ labelRow.add(label);
543
+ const input = new InputRenderable2(renderer, {
544
+ id: "assignee-input",
545
+ width: 39,
546
+ height: 1,
547
+ placeholder: "Enter username (empty to unassign)",
548
+ textColor: COLORS.text,
549
+ placeholderColor: COLORS.textDim,
550
+ backgroundColor: COLORS.inputBg,
551
+ focusedBackgroundColor: COLORS.inputBg,
552
+ cursorColor: COLORS.cursor
553
+ });
554
+ const spacer2 = new BoxRenderable6(renderer, { id: "assign-spacer2", width: "100%", height: 1 });
555
+ const doAssign = async () => {
556
+ const assignee = input.value.trim();
557
+ await state.taskService.updateTask(taskId, {
558
+ assignedTo: assignee || null
559
+ });
560
+ closeModal(state);
561
+ await onAssigned();
562
+ };
563
+ const doCancel = () => {
564
+ closeModal(state);
565
+ };
566
+ const buttonRow = createButtonRow(renderer, "assign-task", [
567
+ { label: "Assign", action: doAssign, color: COLORS.success },
568
+ { label: "Cancel", action: doCancel }
569
+ ]);
570
+ dialog.add(titleRow);
571
+ dialog.add(taskRow);
572
+ dialog.add(spacer1);
573
+ dialog.add(labelRow);
574
+ dialog.add(input);
575
+ dialog.add(spacer2);
576
+ dialog.add(buttonRow.container);
577
+ renderer.root.add(overlay);
578
+ setImmediate(() => {
579
+ buttonRow.setFocused(false);
580
+ input.focus();
581
+ });
582
+ state.modalOverlay = overlay;
583
+ state.taskInput = input;
584
+ state.buttonRow = buttonRow;
585
+ state.activeModal = "assignTask";
586
+ input.on(InputRenderableEvents2.ENTER, doAssign);
587
+ }
588
+ // src/components/modals/delete-task.ts
589
+ import { BoxRenderable as BoxRenderable7, TextRenderable as TextRenderable6 } from "@opentui/core";
590
+ async function showDeleteTaskModal(state, onDeleted) {
591
+ const { renderer } = state;
592
+ const taskId = getSelectedTaskId(state);
593
+ if (!taskId) {
594
+ return;
595
+ }
596
+ const task = await state.taskService.getTask(taskId);
597
+ if (!task) {
598
+ return;
599
+ }
600
+ state.selectedTask = task;
601
+ const { overlay, dialog } = createModalOverlay(renderer, {
602
+ id: "delete-task-dialog",
603
+ width: 45,
604
+ height: 10,
605
+ borderColor: COLORS.danger
606
+ });
607
+ const titleRow = new BoxRenderable7(renderer, {
608
+ id: "delete-title-row",
609
+ width: "100%",
610
+ height: 1,
611
+ justifyContent: "center"
612
+ });
613
+ const title = new TextRenderable6(renderer, {
614
+ id: "delete-title",
615
+ content: "Delete Task?",
616
+ fg: COLORS.danger
617
+ });
618
+ titleRow.add(title);
619
+ const spacer1 = new BoxRenderable7(renderer, { id: "delete-spacer1", width: "100%", height: 1 });
620
+ const taskRow = new BoxRenderable7(renderer, {
621
+ id: "delete-task-row",
622
+ width: "100%",
623
+ height: 1
624
+ });
625
+ const taskText = new TextRenderable6(renderer, {
626
+ id: "delete-task-text",
627
+ content: task.title.slice(0, 40),
628
+ fg: COLORS.text
629
+ });
630
+ taskRow.add(taskText);
631
+ const warningRow = new BoxRenderable7(renderer, {
632
+ id: "delete-warning-row",
633
+ width: "100%",
634
+ height: 1
635
+ });
636
+ const warning = new TextRenderable6(renderer, {
637
+ id: "delete-warning",
638
+ content: "This action cannot be undone.",
639
+ fg: COLORS.warning
640
+ });
641
+ warningRow.add(warning);
642
+ const spacer2 = new BoxRenderable7(renderer, { id: "delete-spacer2", width: "100%", height: 2 });
643
+ const hintRow = new BoxRenderable7(renderer, {
644
+ id: "delete-hint-row",
645
+ width: "100%",
646
+ height: 1,
647
+ justifyContent: "center"
648
+ });
649
+ const hint = new TextRenderable6(renderer, {
650
+ id: "delete-hint",
651
+ content: "[y] Delete [n/Esc] Cancel",
652
+ fg: COLORS.textMuted
653
+ });
654
+ hintRow.add(hint);
655
+ dialog.add(titleRow);
656
+ dialog.add(spacer1);
657
+ dialog.add(taskRow);
658
+ dialog.add(warningRow);
659
+ dialog.add(spacer2);
660
+ dialog.add(hintRow);
661
+ renderer.root.add(overlay);
662
+ state.modalOverlay = overlay;
663
+ state.activeModal = "deleteTask";
664
+ state.onModalConfirm = async () => {
665
+ await state.taskService.deleteTask(taskId);
666
+ closeModal(state);
667
+ await onDeleted();
668
+ };
669
+ }
670
+ // src/components/modals/edit-task.ts
671
+ import {
672
+ BoxRenderable as BoxRenderable8,
673
+ InputRenderable as InputRenderable3,
674
+ InputRenderableEvents as InputRenderableEvents3,
675
+ TextRenderable as TextRenderable7
676
+ } from "@opentui/core";
677
+ var DIALOG_WIDTH = 60;
678
+ var DESC_INPUT_HEIGHT = 6;
679
+ async function showEditTaskModal(state, callbacks) {
680
+ const { renderer } = state;
681
+ const task = state.selectedTask;
682
+ if (!task) {
683
+ return;
684
+ }
685
+ state.editTaskState = {
686
+ title: task.title,
687
+ description: task.description ?? "",
688
+ focusedField: "title"
689
+ };
690
+ const dialogHeight = 18;
691
+ const { overlay, dialog } = createModalOverlay(renderer, {
692
+ id: "edit-task-dialog",
693
+ width: DIALOG_WIDTH,
694
+ height: dialogHeight
695
+ });
696
+ const headerRow = new BoxRenderable8(renderer, {
697
+ id: "edit-header-row",
698
+ width: "100%",
699
+ height: 1
700
+ });
701
+ const headerText = new TextRenderable7(renderer, {
702
+ id: "edit-header-text",
703
+ content: `Edit Task: ${truncate(task.title, DIALOG_WIDTH - 20)}`,
704
+ fg: COLORS.accent
705
+ });
706
+ headerRow.add(headerText);
707
+ const spacer1 = new BoxRenderable8(renderer, {
708
+ id: "edit-spacer1",
709
+ width: "100%",
710
+ height: 1
711
+ });
712
+ const titleLabelRow = new BoxRenderable8(renderer, {
713
+ id: "edit-title-label-row",
714
+ width: "100%",
715
+ height: 1
716
+ });
717
+ const titleLabel = new TextRenderable7(renderer, {
718
+ id: "edit-title-label",
719
+ content: "Title",
720
+ fg: COLORS.textMuted
721
+ });
722
+ titleLabelRow.add(titleLabel);
723
+ const titleInput = new InputRenderable3(renderer, {
724
+ id: "edit-title-input",
725
+ width: DIALOG_WIDTH - 6,
726
+ height: 1,
727
+ placeholder: "Task title...",
728
+ textColor: COLORS.text,
729
+ placeholderColor: COLORS.textDim,
730
+ backgroundColor: COLORS.inputBg,
731
+ focusedBackgroundColor: COLORS.inputBg,
732
+ cursorColor: COLORS.cursor
733
+ });
734
+ titleInput.value = task.title;
735
+ const spacer2 = new BoxRenderable8(renderer, {
736
+ id: "edit-spacer2",
737
+ width: "100%",
738
+ height: 1
739
+ });
740
+ const descLabelRow = new BoxRenderable8(renderer, {
741
+ id: "edit-desc-label-row",
742
+ width: "100%",
743
+ height: 1
744
+ });
745
+ const descLabel = new TextRenderable7(renderer, {
746
+ id: "edit-desc-label",
747
+ content: "Description",
748
+ fg: COLORS.textMuted
749
+ });
750
+ descLabelRow.add(descLabel);
751
+ const descInput = new InputRenderable3(renderer, {
752
+ id: "edit-desc-input",
753
+ width: DIALOG_WIDTH - 6,
754
+ height: DESC_INPUT_HEIGHT,
755
+ placeholder: "Task description (optional)...",
756
+ textColor: COLORS.text,
757
+ placeholderColor: COLORS.textDim,
758
+ backgroundColor: COLORS.inputBg,
759
+ focusedBackgroundColor: COLORS.inputBg,
760
+ cursorColor: COLORS.cursor
761
+ });
762
+ descInput.value = task.description ?? "";
763
+ const spacer3 = new BoxRenderable8(renderer, {
764
+ id: "edit-spacer3",
765
+ width: "100%",
766
+ height: 1
767
+ });
768
+ const doSave = async () => {
769
+ const newTitle = titleInput.value.trim();
770
+ const newDescription = descInput.value.trim();
771
+ if (!newTitle) {
772
+ return;
773
+ }
774
+ const hasChanges = newTitle !== task.title || newDescription !== (task.description ?? "");
775
+ if (hasChanges) {
776
+ await state.taskService.updateTask(task.id, {
777
+ title: newTitle,
778
+ description: newDescription || undefined
779
+ });
780
+ }
781
+ closeModal(state);
782
+ state.editTaskState = null;
783
+ await callbacks.onSave();
784
+ };
785
+ const doCancel = () => {
786
+ closeModal(state);
787
+ state.editTaskState = null;
788
+ callbacks.onCancel();
789
+ };
790
+ const buttonRow = createButtonRow(renderer, "edit-task", [
791
+ { label: "Save", action: doSave, color: COLORS.success },
792
+ { label: "Cancel", action: doCancel }
793
+ ]);
794
+ const hintRow = new BoxRenderable8(renderer, {
795
+ id: "edit-hint-row",
796
+ width: "100%",
797
+ height: 1,
798
+ justifyContent: "center"
799
+ });
800
+ const hint = new TextRenderable7(renderer, {
801
+ id: "edit-hint",
802
+ content: "[Tab] Next [Enter] Save [Esc] Cancel",
803
+ fg: COLORS.textDim
804
+ });
805
+ hintRow.add(hint);
806
+ dialog.add(headerRow);
807
+ dialog.add(spacer1);
808
+ dialog.add(titleLabelRow);
809
+ dialog.add(titleInput);
810
+ dialog.add(spacer2);
811
+ dialog.add(descLabelRow);
812
+ dialog.add(descInput);
813
+ dialog.add(spacer3);
814
+ dialog.add(buttonRow.container);
815
+ dialog.add(hintRow);
816
+ renderer.root.add(overlay);
817
+ setImmediate(() => {
818
+ buttonRow.setFocused(false);
819
+ titleInput.focus();
820
+ });
821
+ state.modalOverlay = overlay;
822
+ state.taskInput = titleInput;
823
+ state.buttonRow = buttonRow;
824
+ state.activeModal = "editTask";
825
+ titleInput.on(InputRenderableEvents3.ENTER, doSave);
826
+ state.editTaskRuntime = {
827
+ titleInput,
828
+ descInput,
829
+ doSave,
830
+ doCancel
831
+ };
832
+ }
833
+ function focusNextEditField(state) {
834
+ if (!state.editTaskRuntime || !state.editTaskState)
835
+ return;
836
+ const { titleInput, descInput } = state.editTaskRuntime;
837
+ const { buttonRow } = state;
838
+ switch (state.editTaskState.focusedField) {
839
+ case "title":
840
+ titleInput.blur();
841
+ descInput.focus();
842
+ buttonRow?.setFocused(false);
843
+ state.editTaskState.focusedField = "description";
844
+ break;
845
+ case "description":
846
+ descInput.blur();
847
+ titleInput.blur();
848
+ buttonRow?.setFocused(true);
849
+ state.editTaskState.focusedField = "buttons";
850
+ break;
851
+ case "buttons":
852
+ buttonRow?.setFocused(false);
853
+ titleInput.focus();
854
+ state.editTaskState.focusedField = "title";
855
+ break;
856
+ }
857
+ }
858
+ function cancelEditTask(state) {
859
+ if (!state.editTaskRuntime)
860
+ return;
861
+ state.editTaskRuntime.doCancel();
862
+ }
863
+ // src/components/modals/help.ts
864
+ import { BoxRenderable as BoxRenderable9, TextRenderable as TextRenderable8 } from "@opentui/core";
865
+ var SHORTCUTS = [
866
+ ["<-/-> h/l", "Switch column"],
867
+ ["up/dn j/k", "Navigate tasks"],
868
+ ["Enter", "View task details"],
869
+ ["a", "Add new task"],
870
+ ["m", "Move task (change status)"],
871
+ ["u", "Assign user to task"],
872
+ ["d", "Delete task"],
873
+ ["?", "Show/hide help"],
874
+ ["q", "Quit"]
875
+ ];
876
+ function showHelpModal(state) {
877
+ const { renderer } = state;
878
+ const { overlay, dialog } = createModalOverlay(renderer, {
879
+ id: "help-dialog",
880
+ width: 45,
881
+ height: 17,
882
+ padding: 2
883
+ });
884
+ const titleRow = new BoxRenderable9(renderer, {
885
+ id: "help-title-row",
886
+ width: "100%",
887
+ height: 1
888
+ });
889
+ const title = new TextRenderable8(renderer, {
890
+ id: "help-title",
891
+ content: "Keyboard Shortcuts",
892
+ fg: COLORS.accent
893
+ });
894
+ titleRow.add(title);
895
+ const spacer = new BoxRenderable9(renderer, { id: "help-spacer", width: "100%", height: 1 });
896
+ dialog.add(titleRow);
897
+ dialog.add(spacer);
898
+ for (const [key, desc] of SHORTCUTS) {
899
+ const row = new BoxRenderable9(renderer, {
900
+ id: `help-row-${key}`,
901
+ width: "100%",
902
+ height: 1,
903
+ flexDirection: "row"
904
+ });
905
+ const keyText = new TextRenderable8(renderer, {
906
+ id: `help-key-${key}`,
907
+ content: key.padEnd(12),
908
+ fg: COLORS.accentBright
909
+ });
910
+ const descText = new TextRenderable8(renderer, {
911
+ id: `help-desc-${key}`,
912
+ content: desc,
913
+ fg: COLORS.text
914
+ });
915
+ row.add(keyText);
916
+ row.add(descText);
917
+ dialog.add(row);
918
+ }
919
+ const hintSpacer = new BoxRenderable9(renderer, {
920
+ id: "help-hint-spacer",
921
+ width: "100%",
922
+ height: 1
923
+ });
924
+ const hintRow = new BoxRenderable9(renderer, {
925
+ id: "help-hint-row",
926
+ width: "100%",
927
+ height: 1
928
+ });
929
+ const hint = new TextRenderable8(renderer, {
930
+ id: "help-hint",
931
+ content: "[Esc] or any key to close",
932
+ fg: COLORS.textDim
933
+ });
934
+ hintRow.add(hint);
935
+ dialog.add(hintSpacer);
936
+ dialog.add(hintRow);
937
+ renderer.root.add(overlay);
938
+ state.modalOverlay = overlay;
939
+ state.activeModal = "help";
940
+ }
941
+ // src/components/modals/move-task.ts
942
+ import {
943
+ BoxRenderable as BoxRenderable10,
944
+ SelectRenderable as SelectRenderable2,
945
+ SelectRenderableEvents,
946
+ TextRenderable as TextRenderable9
947
+ } from "@opentui/core";
948
+ async function showMoveTaskModal(state, onMoved) {
949
+ const { renderer, columns } = state;
950
+ const taskId = getSelectedTaskId(state);
951
+ if (!taskId) {
952
+ return;
953
+ }
954
+ const task = await state.taskService.getTask(taskId);
955
+ if (!task) {
956
+ return;
957
+ }
958
+ const otherColumns = columns.filter((c) => c.id !== task.columnId);
959
+ if (otherColumns.length === 0) {
960
+ return;
961
+ }
962
+ const selectHeight = Math.min(otherColumns.length, 6);
963
+ const dialogHeight = selectHeight + 10;
964
+ const { overlay, dialog } = createModalOverlay(renderer, {
965
+ id: "move-task-dialog",
966
+ width: 40,
967
+ height: dialogHeight
968
+ });
969
+ const titleRow = new BoxRenderable10(renderer, {
970
+ id: "move-title-row",
971
+ width: "100%",
972
+ height: 1
973
+ });
974
+ const title = new TextRenderable9(renderer, {
975
+ id: "move-title",
976
+ content: "Move Task",
977
+ fg: COLORS.accent
978
+ });
979
+ titleRow.add(title);
980
+ const taskRow = new BoxRenderable10(renderer, {
981
+ id: "move-task-row",
982
+ width: "100%",
983
+ height: 1
984
+ });
985
+ const taskText = new TextRenderable9(renderer, {
986
+ id: "move-task-text",
987
+ content: task.title.slice(0, 35),
988
+ fg: COLORS.textMuted
989
+ });
990
+ taskRow.add(taskText);
991
+ const spacer1 = new BoxRenderable10(renderer, { id: "move-spacer1", width: "100%", height: 1 });
992
+ const labelRow = new BoxRenderable10(renderer, {
993
+ id: "move-label-row",
994
+ width: "100%",
995
+ height: 1
996
+ });
997
+ const label = new TextRenderable9(renderer, {
998
+ id: "move-label",
999
+ content: "Select column:",
1000
+ fg: COLORS.text
1001
+ });
1002
+ labelRow.add(label);
1003
+ const columnSelect = new SelectRenderable2(renderer, {
1004
+ id: "column-select",
1005
+ width: "100%",
1006
+ height: selectHeight,
1007
+ backgroundColor: COLORS.panel,
1008
+ textColor: COLORS.text,
1009
+ showDescription: false,
1010
+ options: otherColumns.map((col) => ({
1011
+ name: col.name,
1012
+ description: "",
1013
+ value: col.id
1014
+ })),
1015
+ selectedBackgroundColor: COLORS.inputBg,
1016
+ selectedTextColor: COLORS.accentBright
1017
+ });
1018
+ const spacer2 = new BoxRenderable10(renderer, { id: "move-spacer2", width: "100%", height: 1 });
1019
+ const hintRow = new BoxRenderable10(renderer, {
1020
+ id: "move-hint-row",
1021
+ width: "100%",
1022
+ height: 1
1023
+ });
1024
+ const hint = new TextRenderable9(renderer, {
1025
+ id: "move-hint",
1026
+ content: "[Enter] Move [Esc] Cancel",
1027
+ fg: COLORS.textDim
1028
+ });
1029
+ hintRow.add(hint);
1030
+ dialog.add(titleRow);
1031
+ dialog.add(taskRow);
1032
+ dialog.add(spacer1);
1033
+ dialog.add(labelRow);
1034
+ dialog.add(columnSelect);
1035
+ dialog.add(spacer2);
1036
+ dialog.add(hintRow);
1037
+ renderer.root.add(overlay);
1038
+ setImmediate(() => columnSelect.focus());
1039
+ state.modalOverlay = overlay;
1040
+ state.activeModal = "moveTask";
1041
+ columnSelect.on(SelectRenderableEvents.ITEM_SELECTED, async () => {
1042
+ const selected = columnSelect.getSelectedOption();
1043
+ if (selected?.value) {
1044
+ await state.taskService.moveTask(taskId, selected.value);
1045
+ }
1046
+ closeModal(state);
1047
+ await onMoved();
1048
+ });
1049
+ }
1050
+ // src/components/modals/onboarding.ts
1051
+ import {
1052
+ BoxRenderable as BoxRenderable11,
1053
+ InputRenderable as InputRenderable4,
1054
+ InputRenderableEvents as InputRenderableEvents4,
1055
+ TextRenderable as TextRenderable10
1056
+ } from "@opentui/core";
1057
+ async function showOnboarding(renderer) {
1058
+ return new Promise((resolvePromise) => {
1059
+ const container = new BoxRenderable11(renderer, {
1060
+ id: "onboarding",
1061
+ width: "100%",
1062
+ height: "100%",
1063
+ flexDirection: "column",
1064
+ justifyContent: "center",
1065
+ alignItems: "center",
1066
+ backgroundColor: COLORS.bg
1067
+ });
1068
+ const card = new BoxRenderable11(renderer, {
1069
+ id: "card",
1070
+ width: 52,
1071
+ height: 13,
1072
+ flexDirection: "column",
1073
+ border: true,
1074
+ borderStyle: "rounded",
1075
+ borderColor: COLORS.accent,
1076
+ backgroundColor: COLORS.panel,
1077
+ paddingTop: 1,
1078
+ paddingBottom: 1,
1079
+ paddingLeft: 2,
1080
+ paddingRight: 2
1081
+ });
1082
+ const titleRow = new BoxRenderable11(renderer, {
1083
+ id: "title-row",
1084
+ width: "100%",
1085
+ height: 1
1086
+ });
1087
+ const title = new TextRenderable10(renderer, {
1088
+ id: "title",
1089
+ content: "Welcome to Kaban",
1090
+ fg: COLORS.accent
1091
+ });
1092
+ titleRow.add(title);
1093
+ const subtitleRow = new BoxRenderable11(renderer, {
1094
+ id: "subtitle-row",
1095
+ width: "100%",
1096
+ height: 1
1097
+ });
1098
+ const subtitle = new TextRenderable10(renderer, {
1099
+ id: "subtitle",
1100
+ content: "No board found. Let's create one!",
1101
+ fg: COLORS.textMuted
1102
+ });
1103
+ subtitleRow.add(subtitle);
1104
+ const spacer1 = new BoxRenderable11(renderer, { id: "spacer1", width: "100%", height: 1 });
1105
+ const labelRow = new BoxRenderable11(renderer, {
1106
+ id: "label-row",
1107
+ width: "100%",
1108
+ height: 1
1109
+ });
1110
+ const label = new TextRenderable10(renderer, {
1111
+ id: "label",
1112
+ content: "Board name:",
1113
+ fg: COLORS.text
1114
+ });
1115
+ labelRow.add(label);
1116
+ const input = new InputRenderable4(renderer, {
1117
+ id: "board-name-input",
1118
+ width: 44,
1119
+ height: 1,
1120
+ placeholder: "My Project Board",
1121
+ textColor: COLORS.text,
1122
+ placeholderColor: COLORS.textDim,
1123
+ backgroundColor: COLORS.inputBg,
1124
+ focusedBackgroundColor: COLORS.inputBg,
1125
+ cursorColor: COLORS.cursor
1126
+ });
1127
+ const spacer2 = new BoxRenderable11(renderer, { id: "spacer2", width: "100%", height: 1 });
1128
+ const doCreate = () => {
1129
+ renderer.keyInput.off("keypress", keyHandler);
1130
+ const boardName = input.value.trim() || "Kaban Board";
1131
+ container.destroy();
1132
+ resolvePromise(boardName);
1133
+ };
1134
+ const doQuit = () => {
1135
+ renderer.keyInput.off("keypress", keyHandler);
1136
+ renderer.destroy();
1137
+ process.exit(0);
1138
+ };
1139
+ const buttonRow = createButtonRow(renderer, "onboarding", [
1140
+ { label: "Create", action: doCreate, color: COLORS.success },
1141
+ { label: "Quit", action: doQuit, color: COLORS.danger }
1142
+ ]);
1143
+ card.add(titleRow);
1144
+ card.add(subtitleRow);
1145
+ card.add(spacer1);
1146
+ card.add(labelRow);
1147
+ card.add(input);
1148
+ card.add(spacer2);
1149
+ card.add(buttonRow.container);
1150
+ container.add(card);
1151
+ renderer.root.add(container);
1152
+ input.focus();
1153
+ input.on(InputRenderableEvents4.ENTER, doCreate);
1154
+ const keyBindings = {
1155
+ tab: () => {
1156
+ input.blur();
1157
+ buttonRow.setFocused(true);
1158
+ },
1159
+ down: () => {
1160
+ input.blur();
1161
+ buttonRow.setFocused(true);
1162
+ },
1163
+ up: () => {
1164
+ buttonRow.setFocused(false);
1165
+ input.focus();
1166
+ },
1167
+ left: () => buttonRow.selectPrev(),
1168
+ right: () => buttonRow.selectNext(),
1169
+ return: () => {
1170
+ if (!input.focused)
1171
+ buttonRow.triggerSelected();
1172
+ },
1173
+ escape: doQuit
1174
+ };
1175
+ const keyHandler = (key) => {
1176
+ keyBindings[key.name]?.();
1177
+ };
1178
+ renderer.keyInput.on("keypress", keyHandler);
1179
+ });
1180
+ }
1181
+ // src/components/modals/quit.ts
1182
+ import { BoxRenderable as BoxRenderable12, TextRenderable as TextRenderable11 } from "@opentui/core";
1183
+ function showQuitModal(state) {
1184
+ const { renderer } = state;
1185
+ const { overlay, dialog } = createModalOverlay(renderer, {
1186
+ id: "quit-dialog",
1187
+ width: 32,
1188
+ height: 8,
1189
+ borderColor: COLORS.danger
1190
+ });
1191
+ const titleRow = new BoxRenderable12(renderer, {
1192
+ id: "quit-title-row",
1193
+ width: "100%",
1194
+ height: 1,
1195
+ justifyContent: "center"
1196
+ });
1197
+ const title = new TextRenderable11(renderer, {
1198
+ id: "quit-title",
1199
+ content: "Quit Kaban?",
1200
+ fg: COLORS.danger
1201
+ });
1202
+ titleRow.add(title);
1203
+ const spacer = new BoxRenderable12(renderer, { id: "quit-spacer", width: "100%", height: 2 });
1204
+ const hintRow = new BoxRenderable12(renderer, {
1205
+ id: "quit-hint-row",
1206
+ width: "100%",
1207
+ height: 1,
1208
+ justifyContent: "center"
1209
+ });
1210
+ const hint = new TextRenderable11(renderer, {
1211
+ id: "quit-hint",
1212
+ content: "[y] Yes [n/Esc] No",
1213
+ fg: COLORS.textMuted
1214
+ });
1215
+ hintRow.add(hint);
1216
+ dialog.add(titleRow);
1217
+ dialog.add(spacer);
1218
+ dialog.add(hintRow);
1219
+ renderer.root.add(overlay);
1220
+ state.modalOverlay = overlay;
1221
+ state.activeModal = "quit";
1222
+ }
1223
+ // src/components/modals/view-task.ts
1224
+ import { BoxRenderable as BoxRenderable13, TextRenderable as TextRenderable12 } from "@opentui/core";
1225
+ var DIALOG_WIDTH2 = 60;
1226
+ var DESC_VISIBLE_LINES = 4;
1227
+ var LABEL_WIDTH = 12;
1228
+ function formatDate(date) {
1229
+ if (!date)
1230
+ return "—";
1231
+ return date.toLocaleString("en-US", {
1232
+ month: "short",
1233
+ day: "numeric",
1234
+ year: "numeric",
1235
+ hour: "2-digit",
1236
+ minute: "2-digit"
1237
+ });
1238
+ }
1239
+ function padLabel(label) {
1240
+ return label.padEnd(LABEL_WIDTH);
1241
+ }
1242
+ async function showViewTaskModal(state, actions, taskIdOverride) {
1243
+ const { renderer, columns } = state;
1244
+ const taskId = taskIdOverride ?? getSelectedTaskId(state);
1245
+ if (!taskId) {
1246
+ return;
1247
+ }
1248
+ const task = await state.taskService.getTask(taskId);
1249
+ if (!task) {
1250
+ return;
1251
+ }
1252
+ blurCurrentColumnSelect(state);
1253
+ const column = columns.find((c) => c.id === task.columnId);
1254
+ const columnName = column?.name ?? task.columnId;
1255
+ const statusColor = getStatusColor(task.columnId);
1256
+ const hasDescription = task.description && task.description.trim().length > 0;
1257
+ const allDescLines = hasDescription ? task.description?.split(`
1258
+ `) ?? [] : [];
1259
+ const totalDescLines = allDescLines.length;
1260
+ state.viewTaskState = {
1261
+ descriptionScrollOffset: 0,
1262
+ showCopiedFeedback: false
1263
+ };
1264
+ const dialogHeight = 24;
1265
+ const { overlay, dialog } = createModalOverlay(renderer, {
1266
+ id: "view-task-dialog",
1267
+ width: DIALOG_WIDTH2,
1268
+ height: dialogHeight
1269
+ });
1270
+ const headerDivider = createSectionDivider(renderer, {
1271
+ label: "Task Details",
1272
+ width: DIALOG_WIDTH2 - 4,
1273
+ id: "view-header"
1274
+ });
1275
+ const spacerHeader = new BoxRenderable13(renderer, {
1276
+ id: "view-spacer-header",
1277
+ width: "100%",
1278
+ height: 1
1279
+ });
1280
+ const titleRow = new BoxRenderable13(renderer, {
1281
+ id: "view-title-row",
1282
+ width: "100%",
1283
+ height: 1,
1284
+ flexDirection: "row",
1285
+ justifyContent: "space-between"
1286
+ });
1287
+ const taskTitle = new TextRenderable12(renderer, {
1288
+ id: "view-task-title",
1289
+ content: truncate(task.title, DIALOG_WIDTH2 - 14),
1290
+ fg: COLORS.text
1291
+ });
1292
+ const editHint = new TextRenderable12(renderer, {
1293
+ id: "view-edit-hint",
1294
+ content: "[e]dit",
1295
+ fg: COLORS.textDim
1296
+ });
1297
+ titleRow.add(taskTitle);
1298
+ titleRow.add(editHint);
1299
+ const idRow = new BoxRenderable13(renderer, {
1300
+ id: "view-id-row",
1301
+ width: "100%",
1302
+ height: 1,
1303
+ flexDirection: "row",
1304
+ justifyContent: "space-between"
1305
+ });
1306
+ const idValue = new TextRenderable12(renderer, {
1307
+ id: "view-id-value",
1308
+ content: truncateMiddle(task.id, DIALOG_WIDTH2 - 14),
1309
+ fg: COLORS.textDim
1310
+ });
1311
+ const copyHint = new TextRenderable12(renderer, {
1312
+ id: "view-copy-hint",
1313
+ content: "[c]opy",
1314
+ fg: COLORS.textDim
1315
+ });
1316
+ idRow.add(idValue);
1317
+ idRow.add(copyHint);
1318
+ const statusDivider = createSectionDivider(renderer, {
1319
+ label: "Status",
1320
+ width: DIALOG_WIDTH2 - 4,
1321
+ id: "view-status"
1322
+ });
1323
+ const columnRow = new BoxRenderable13(renderer, {
1324
+ id: "view-column-row",
1325
+ width: "100%",
1326
+ height: 1,
1327
+ flexDirection: "row"
1328
+ });
1329
+ const columnLabel = new TextRenderable12(renderer, {
1330
+ id: "view-column-label",
1331
+ content: padLabel("Column"),
1332
+ fg: COLORS.textMuted
1333
+ });
1334
+ const columnBullet = new TextRenderable12(renderer, {
1335
+ id: "view-column-bullet",
1336
+ content: "● ",
1337
+ fg: statusColor
1338
+ });
1339
+ const columnValue = new TextRenderable12(renderer, {
1340
+ id: "view-column-value",
1341
+ content: columnName,
1342
+ fg: COLORS.text
1343
+ });
1344
+ columnRow.add(columnLabel);
1345
+ columnRow.add(columnBullet);
1346
+ columnRow.add(columnValue);
1347
+ const assigneeRow = new BoxRenderable13(renderer, {
1348
+ id: "view-assignee-row",
1349
+ width: "100%",
1350
+ height: 1,
1351
+ flexDirection: "row"
1352
+ });
1353
+ const assigneeLabel = new TextRenderable12(renderer, {
1354
+ id: "view-assignee-label",
1355
+ content: padLabel("Assignee"),
1356
+ fg: COLORS.textMuted
1357
+ });
1358
+ const assigneeValue = new TextRenderable12(renderer, {
1359
+ id: "view-assignee-value",
1360
+ content: task.assignedTo ?? "— unassigned",
1361
+ fg: task.assignedTo ? COLORS.success : COLORS.textDim
1362
+ });
1363
+ assigneeRow.add(assigneeLabel);
1364
+ assigneeRow.add(assigneeValue);
1365
+ const creatorRow = new BoxRenderable13(renderer, {
1366
+ id: "view-creator-row",
1367
+ width: "100%",
1368
+ height: 1,
1369
+ flexDirection: "row"
1370
+ });
1371
+ const creatorLabel = new TextRenderable12(renderer, {
1372
+ id: "view-creator-label",
1373
+ content: padLabel("Creator"),
1374
+ fg: COLORS.textMuted
1375
+ });
1376
+ const creatorValue = new TextRenderable12(renderer, {
1377
+ id: "view-creator-value",
1378
+ content: task.createdBy,
1379
+ fg: COLORS.text
1380
+ });
1381
+ creatorRow.add(creatorLabel);
1382
+ creatorRow.add(creatorValue);
1383
+ const labelsRow = new BoxRenderable13(renderer, {
1384
+ id: "view-labels-row",
1385
+ width: "100%",
1386
+ height: 1,
1387
+ flexDirection: "row"
1388
+ });
1389
+ const labelsLabel = new TextRenderable12(renderer, {
1390
+ id: "view-labels-label",
1391
+ content: padLabel("Labels"),
1392
+ fg: COLORS.textMuted
1393
+ });
1394
+ const labelsValue = new TextRenderable12(renderer, {
1395
+ id: "view-labels-value",
1396
+ content: "— none",
1397
+ fg: COLORS.textDim
1398
+ });
1399
+ labelsRow.add(labelsLabel);
1400
+ labelsRow.add(labelsValue);
1401
+ const timelineDivider = createSectionDivider(renderer, {
1402
+ label: "Timeline",
1403
+ width: DIALOG_WIDTH2 - 4,
1404
+ id: "view-timeline"
1405
+ });
1406
+ const createdRow = new BoxRenderable13(renderer, {
1407
+ id: "view-created-row",
1408
+ width: "100%",
1409
+ height: 1,
1410
+ flexDirection: "row"
1411
+ });
1412
+ const createdLabel = new TextRenderable12(renderer, {
1413
+ id: "view-created-label",
1414
+ content: padLabel("Created"),
1415
+ fg: COLORS.textMuted
1416
+ });
1417
+ const createdValue = new TextRenderable12(renderer, {
1418
+ id: "view-created-value",
1419
+ content: formatDate(task.createdAt),
1420
+ fg: COLORS.textDim
1421
+ });
1422
+ createdRow.add(createdLabel);
1423
+ createdRow.add(createdValue);
1424
+ const updatedRow = new BoxRenderable13(renderer, {
1425
+ id: "view-updated-row",
1426
+ width: "100%",
1427
+ height: 1,
1428
+ flexDirection: "row",
1429
+ justifyContent: "space-between"
1430
+ });
1431
+ const updatedLeft = new BoxRenderable13(renderer, {
1432
+ id: "view-updated-left",
1433
+ height: 1,
1434
+ flexDirection: "row"
1435
+ });
1436
+ const updatedLabel = new TextRenderable12(renderer, {
1437
+ id: "view-updated-label",
1438
+ content: padLabel("Updated"),
1439
+ fg: COLORS.textMuted
1440
+ });
1441
+ const updatedValue = new TextRenderable12(renderer, {
1442
+ id: "view-updated-value",
1443
+ content: formatDate(task.updatedAt),
1444
+ fg: COLORS.textDim
1445
+ });
1446
+ updatedLeft.add(updatedLabel);
1447
+ updatedLeft.add(updatedValue);
1448
+ const relativeTime = new TextRenderable12(renderer, {
1449
+ id: "view-relative-time",
1450
+ content: `(${formatRelativeTime(task.updatedAt)})`,
1451
+ fg: COLORS.textDim
1452
+ });
1453
+ updatedRow.add(updatedLeft);
1454
+ updatedRow.add(relativeTime);
1455
+ const descDivider = createSectionDivider(renderer, {
1456
+ label: "Description",
1457
+ width: DIALOG_WIDTH2 - 4,
1458
+ id: "view-desc"
1459
+ });
1460
+ const descContainer = new BoxRenderable13(renderer, {
1461
+ id: "view-desc-container",
1462
+ width: "100%",
1463
+ height: DESC_VISIBLE_LINES,
1464
+ flexDirection: "column"
1465
+ });
1466
+ const descLineRenderables = [];
1467
+ for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1468
+ const line = new TextRenderable12(renderer, {
1469
+ id: `view-desc-line-${i}`,
1470
+ content: " ",
1471
+ fg: COLORS.text
1472
+ });
1473
+ descLineRenderables.push(line);
1474
+ descContainer.add(line);
1475
+ }
1476
+ function updateDescriptionContent(scrollOffset) {
1477
+ if (!hasDescription) {
1478
+ descLineRenderables[0].content = "— no description";
1479
+ descLineRenderables[0].fg = COLORS.textDim;
1480
+ for (let i = 1;i < DESC_VISIBLE_LINES; i++) {
1481
+ descLineRenderables[i].content = " ";
1482
+ }
1483
+ return;
1484
+ }
1485
+ const visibleLines = allDescLines.slice(scrollOffset, scrollOffset + DESC_VISIBLE_LINES);
1486
+ const hasMore = scrollOffset + DESC_VISIBLE_LINES < totalDescLines;
1487
+ const hasLess = scrollOffset > 0;
1488
+ for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1489
+ const lineContent = visibleLines[i] ?? "";
1490
+ const isLastLine = i === DESC_VISIBLE_LINES - 1;
1491
+ let displayContent = truncate(lineContent, DIALOG_WIDTH2 - 12);
1492
+ if (isLastLine && hasMore) {
1493
+ const remaining = totalDescLines - scrollOffset - DESC_VISIBLE_LINES;
1494
+ displayContent = `${truncate(lineContent, DIALOG_WIDTH2 - 18)} ▼ ${remaining}+`;
1495
+ }
1496
+ if (i === 0 && hasLess) {
1497
+ displayContent = `▲ ${scrollOffset}+ ${truncate(lineContent, DIALOG_WIDTH2 - 18)}`;
1498
+ }
1499
+ descLineRenderables[i].content = displayContent || " ";
1500
+ descLineRenderables[i].fg = i === 0 && hasLess || isLastLine && hasMore ? COLORS.textDim : COLORS.text;
1501
+ }
1502
+ }
1503
+ updateDescriptionContent(0);
1504
+ const footerDivider = new BoxRenderable13(renderer, {
1505
+ id: "view-footer-divider",
1506
+ width: "100%",
1507
+ height: 1
1508
+ });
1509
+ const footerLine = new TextRenderable12(renderer, {
1510
+ id: "view-footer-line",
1511
+ content: "─".repeat(DIALOG_WIDTH2 - 4),
1512
+ fg: COLORS.border
1513
+ });
1514
+ footerDivider.add(footerLine);
1515
+ const actionsRow = new BoxRenderable13(renderer, {
1516
+ id: "view-actions-row",
1517
+ width: "100%",
1518
+ height: 1,
1519
+ flexDirection: "row",
1520
+ justifyContent: "space-between"
1521
+ });
1522
+ const actionsLeft = new BoxRenderable13(renderer, {
1523
+ id: "view-actions-left",
1524
+ height: 1,
1525
+ flexDirection: "row",
1526
+ gap: 2
1527
+ });
1528
+ const moveAction = new TextRenderable12(renderer, {
1529
+ id: "view-action-move",
1530
+ content: "[m] Move",
1531
+ fg: COLORS.textMuted
1532
+ });
1533
+ const assignAction = new TextRenderable12(renderer, {
1534
+ id: "view-action-assign",
1535
+ content: "[u] Assign",
1536
+ fg: COLORS.textMuted
1537
+ });
1538
+ const editAction = new TextRenderable12(renderer, {
1539
+ id: "view-action-edit",
1540
+ content: "[e] Edit",
1541
+ fg: COLORS.textMuted
1542
+ });
1543
+ const deleteAction = new TextRenderable12(renderer, {
1544
+ id: "view-action-delete",
1545
+ content: "[d] Delete",
1546
+ fg: COLORS.danger
1547
+ });
1548
+ actionsLeft.add(moveAction);
1549
+ actionsLeft.add(assignAction);
1550
+ actionsLeft.add(editAction);
1551
+ actionsLeft.add(deleteAction);
1552
+ const escAction = new TextRenderable12(renderer, {
1553
+ id: "view-action-esc",
1554
+ content: "[Esc]",
1555
+ fg: COLORS.textDim
1556
+ });
1557
+ actionsRow.add(actionsLeft);
1558
+ actionsRow.add(escAction);
1559
+ dialog.add(headerDivider);
1560
+ dialog.add(spacerHeader);
1561
+ dialog.add(titleRow);
1562
+ dialog.add(idRow);
1563
+ dialog.add(statusDivider);
1564
+ dialog.add(columnRow);
1565
+ dialog.add(assigneeRow);
1566
+ dialog.add(creatorRow);
1567
+ dialog.add(labelsRow);
1568
+ dialog.add(timelineDivider);
1569
+ dialog.add(createdRow);
1570
+ dialog.add(updatedRow);
1571
+ dialog.add(descDivider);
1572
+ dialog.add(descContainer);
1573
+ dialog.add(footerDivider);
1574
+ dialog.add(actionsRow);
1575
+ renderer.root.add(overlay);
1576
+ state.modalOverlay = overlay;
1577
+ state.activeModal = "viewTask";
1578
+ state.selectedTask = task;
1579
+ state.viewTaskRuntime = {
1580
+ updateDescriptionContent,
1581
+ totalDescLines,
1582
+ actions,
1583
+ idValue,
1584
+ copyHint,
1585
+ taskId: task.id,
1586
+ copyTimeoutId: null
1587
+ };
1588
+ }
1589
+ function scrollViewTaskDescription(state, direction) {
1590
+ if (!state.viewTaskState || !state.viewTaskRuntime)
1591
+ return;
1592
+ const { totalDescLines, updateDescriptionContent } = state.viewTaskRuntime;
1593
+ const maxOffset = Math.max(0, totalDescLines - DESC_VISIBLE_LINES);
1594
+ if (direction === "down") {
1595
+ state.viewTaskState.descriptionScrollOffset = Math.min(state.viewTaskState.descriptionScrollOffset + 1, maxOffset);
1596
+ } else {
1597
+ state.viewTaskState.descriptionScrollOffset = Math.max(state.viewTaskState.descriptionScrollOffset - 1, 0);
1598
+ }
1599
+ updateDescriptionContent(state.viewTaskState.descriptionScrollOffset);
1600
+ }
1601
+ async function copyTaskId(state) {
1602
+ if (!state.viewTaskState || !state.viewTaskRuntime)
1603
+ return;
1604
+ const { taskId, idValue, copyHint } = state.viewTaskRuntime;
1605
+ if (state.viewTaskRuntime.copyTimeoutId) {
1606
+ clearTimeout(state.viewTaskRuntime.copyTimeoutId);
1607
+ state.viewTaskRuntime.copyTimeoutId = null;
1608
+ }
1609
+ try {
1610
+ const { exec } = await import("node:child_process");
1611
+ const { promisify } = await import("node:util");
1612
+ const execAsync = promisify(exec);
1613
+ if (process.platform === "darwin") {
1614
+ await execAsync(`echo -n "${taskId}" | pbcopy`);
1615
+ } else if (process.platform === "linux") {
1616
+ await execAsync(`echo -n "${taskId}" | xclip -selection clipboard`);
1617
+ }
1618
+ const originalContent = idValue.content.toString();
1619
+ idValue.content = "Copied!";
1620
+ idValue.fg = COLORS.success;
1621
+ copyHint.content = "✓";
1622
+ copyHint.fg = COLORS.success;
1623
+ state.viewTaskRuntime.copyTimeoutId = setTimeout(() => {
1624
+ if (state.viewTaskRuntime) {
1625
+ idValue.content = originalContent;
1626
+ idValue.fg = COLORS.textDim;
1627
+ copyHint.content = "[c]opy";
1628
+ copyHint.fg = COLORS.textDim;
1629
+ state.viewTaskRuntime.copyTimeoutId = null;
1630
+ }
1631
+ }, 1000);
1632
+ } catch {
1633
+ idValue.content = "Failed!";
1634
+ idValue.fg = COLORS.danger;
1635
+ copyHint.content = "✗";
1636
+ copyHint.fg = COLORS.danger;
1637
+ state.viewTaskRuntime.copyTimeoutId = setTimeout(() => {
1638
+ if (state.viewTaskRuntime) {
1639
+ idValue.content = truncateMiddle(taskId, DIALOG_WIDTH2 - 14);
1640
+ idValue.fg = COLORS.textDim;
1641
+ copyHint.content = "[c]opy";
1642
+ copyHint.fg = COLORS.textDim;
1643
+ state.viewTaskRuntime.copyTimeoutId = null;
1644
+ }
1645
+ }, 1000);
1646
+ }
1647
+ }
1648
+ // src/lib/keybindings.ts
1649
+ var WILDCARD = "*";
1650
+ var navigateLeft = async (state) => {
1651
+ state.currentColumnIndex = Math.max(0, state.currentColumnIndex - 1);
1652
+ await refreshBoard(state);
1653
+ };
1654
+ var navigateRight = async (state) => {
1655
+ state.currentColumnIndex = Math.min(state.columns.length - 1, state.currentColumnIndex + 1);
1656
+ await refreshBoard(state);
1657
+ };
1658
+ var quit = (state) => {
1659
+ state.renderer.destroy();
1660
+ process.exit(0);
1661
+ };
1662
+ var openMoveModal = async (state) => {
1663
+ const taskId = getSelectedTaskId(state);
1664
+ if (taskId) {
1665
+ await showMoveTaskModal(state, () => refreshBoard(state));
1666
+ }
1667
+ };
1668
+ var openAssignModal = async (state) => {
1669
+ const taskId = getSelectedTaskId(state);
1670
+ if (taskId) {
1671
+ await showAssignTaskModal(state, () => refreshBoard(state));
1672
+ }
1673
+ };
1674
+ var openDeleteModal = async (state) => {
1675
+ const taskId = getSelectedTaskId(state);
1676
+ if (taskId) {
1677
+ await showDeleteTaskModal(state, () => refreshBoard(state));
1678
+ }
1679
+ };
1680
+ var openEditModal = async (state) => {
1681
+ const task = state.selectedTask;
1682
+ if (!task)
1683
+ return;
1684
+ const preservedTaskId = task.id;
1685
+ closeModal(state);
1686
+ state.selectedTask = task;
1687
+ await showEditTaskModal(state, {
1688
+ onSave: async () => {
1689
+ await refreshBoard(state);
1690
+ await openViewModalForTask(state, preservedTaskId);
1691
+ },
1692
+ onCancel: async () => {
1693
+ await openViewModalForTask(state, preservedTaskId);
1694
+ }
1695
+ });
1696
+ };
1697
+ async function openViewModalForTask(state, taskIdOverride) {
1698
+ const taskId = taskIdOverride ?? getSelectedTaskId(state);
1699
+ if (taskId) {
1700
+ await showViewTaskModal(state, {
1701
+ onMove: async () => {
1702
+ await showMoveTaskModal(state, () => refreshBoard(state));
1703
+ },
1704
+ onAssign: async () => {
1705
+ await showAssignTaskModal(state, () => refreshBoard(state));
1706
+ },
1707
+ onDelete: async () => {
1708
+ await showDeleteTaskModal(state, () => refreshBoard(state));
1709
+ },
1710
+ onEdit: async () => {
1711
+ await openEditModal(state);
1712
+ }
1713
+ }, taskIdOverride);
1714
+ }
1715
+ }
1716
+ var openViewModal = (state) => openViewModalForTask(state);
1717
+ var buttonSelectPrev = (state) => {
1718
+ if (state.taskInput?.focused)
1719
+ return;
1720
+ state.buttonRow?.selectPrev();
1721
+ };
1722
+ var buttonSelectNext = (state) => {
1723
+ if (state.taskInput?.focused)
1724
+ return;
1725
+ state.buttonRow?.selectNext();
1726
+ };
1727
+ var buttonTrigger = (state) => {
1728
+ if (state.taskInput?.focused)
1729
+ return;
1730
+ state.buttonRow?.triggerSelected();
1731
+ };
1732
+ var editTaskSave = async (state) => {
1733
+ if (!state.editTaskRuntime)
1734
+ return;
1735
+ const focusedField = state.editTaskState?.focusedField;
1736
+ if (focusedField === "title") {
1737
+ return;
1738
+ }
1739
+ if (focusedField === "buttons") {
1740
+ state.buttonRow?.triggerSelected();
1741
+ } else {
1742
+ await state.editTaskRuntime.doSave();
1743
+ }
1744
+ };
1745
+ var focusButtons = (state) => {
1746
+ state.taskInput?.blur();
1747
+ state.buttonRow?.setFocused(true);
1748
+ };
1749
+ var focusInput = (state) => {
1750
+ state.buttonRow?.setFocused(false);
1751
+ state.taskInput?.focus();
1752
+ };
1753
+ var confirmModal = async (state) => {
1754
+ await state.onModalConfirm?.();
1755
+ };
1756
+ var modalBindings = {
1757
+ none: {
1758
+ q: showQuitModal,
1759
+ escape: showQuitModal,
1760
+ left: navigateLeft,
1761
+ h: navigateLeft,
1762
+ right: navigateRight,
1763
+ l: navigateRight,
1764
+ a: (state) => showAddTaskModal(state, () => refreshBoard(state)),
1765
+ m: openMoveModal,
1766
+ u: openAssignModal,
1767
+ d: openDeleteModal,
1768
+ return: openViewModal,
1769
+ "?": showHelpModal
1770
+ },
1771
+ addTask: {
1772
+ escape: closeModal,
1773
+ left: buttonSelectPrev,
1774
+ right: buttonSelectNext,
1775
+ tab: focusButtons,
1776
+ down: focusButtons,
1777
+ up: focusInput,
1778
+ return: buttonTrigger
1779
+ },
1780
+ moveTask: {
1781
+ escape: closeModal
1782
+ },
1783
+ assignTask: {
1784
+ escape: closeModal,
1785
+ left: buttonSelectPrev,
1786
+ right: buttonSelectNext,
1787
+ tab: focusButtons,
1788
+ down: focusButtons,
1789
+ up: focusInput,
1790
+ return: buttonTrigger
1791
+ },
1792
+ deleteTask: {
1793
+ y: confirmModal,
1794
+ n: closeModal,
1795
+ escape: closeModal
1796
+ },
1797
+ viewTask: {
1798
+ escape: closeModal,
1799
+ left: buttonSelectPrev,
1800
+ right: buttonSelectNext,
1801
+ tab: focusButtons,
1802
+ return: buttonTrigger,
1803
+ m: async (state) => {
1804
+ closeModal(state);
1805
+ await openMoveModal(state);
1806
+ },
1807
+ u: async (state) => {
1808
+ closeModal(state);
1809
+ await openAssignModal(state);
1810
+ },
1811
+ d: async (state) => {
1812
+ closeModal(state);
1813
+ await openDeleteModal(state);
1814
+ },
1815
+ e: openEditModal,
1816
+ c: copyTaskId,
1817
+ j: (state) => scrollViewTaskDescription(state, "down"),
1818
+ k: (state) => scrollViewTaskDescription(state, "up"),
1819
+ down: (state) => scrollViewTaskDescription(state, "down"),
1820
+ up: (state) => scrollViewTaskDescription(state, "up")
1821
+ },
1822
+ editTask: {
1823
+ escape: cancelEditTask,
1824
+ tab: focusNextEditField,
1825
+ left: buttonSelectPrev,
1826
+ right: buttonSelectNext,
1827
+ return: editTaskSave
1828
+ },
1829
+ help: {
1830
+ [WILDCARD]: closeModal
1831
+ },
1832
+ quit: {
1833
+ y: quit,
1834
+ n: closeModal,
1835
+ escape: closeModal
1836
+ }
1837
+ };
1838
+ function handleKeypress(state, key) {
1839
+ const bindings = modalBindings[state.activeModal];
1840
+ const handler = bindings[key.name] ?? bindings[WILDCARD];
1841
+ return handler?.(state);
1842
+ }
1843
+
1844
+ // src/lib/project.ts
1845
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1846
+ import { resolve } from "node:path";
1847
+ import { BoardService, createDb, DEFAULT_CONFIG, initializeSchema } from "@kaban-board/core";
1848
+ function findKabanRoot(startDir) {
1849
+ let dir = startDir;
1850
+ while (dir !== "/") {
1851
+ if (existsSync(resolve(dir, ".kaban"))) {
1852
+ return dir;
1853
+ }
1854
+ dir = resolve(dir, "..");
1855
+ }
1856
+ return null;
1857
+ }
1858
+ function getKabanPaths(root) {
1859
+ const kabanDir = resolve(root, ".kaban");
1860
+ return {
1861
+ kabanDir,
1862
+ dbPath: resolve(kabanDir, "board.db"),
1863
+ configPath: resolve(kabanDir, "config.json")
1864
+ };
1865
+ }
1866
+ async function initializeProject(root, boardName) {
1867
+ const { kabanDir, dbPath, configPath } = getKabanPaths(root);
1868
+ mkdirSync(kabanDir, { recursive: true });
1869
+ const config = {
1870
+ ...DEFAULT_CONFIG,
1871
+ board: { name: boardName }
1872
+ };
1873
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1874
+ const db = createDb(dbPath);
1875
+ await initializeSchema(db);
1876
+ const boardService = new BoardService(db);
1877
+ await boardService.initializeBoard(config);
1878
+ return { db, boardService };
1879
+ }
1880
+
1881
+ // src/index.ts
1882
+ var POLL_INTERVAL_MS = 500;
1883
+ async function main() {
1884
+ const renderer = await createCliRenderer({
1885
+ exitOnCtrlC: true
1886
+ });
1887
+ let projectRoot = findKabanRoot(process.cwd());
1888
+ let db;
1889
+ let boardService;
1890
+ let taskService;
1891
+ if (!projectRoot) {
1892
+ const boardName = await showOnboarding(renderer);
1893
+ projectRoot = process.cwd();
1894
+ const result = await initializeProject(projectRoot, boardName);
1895
+ db = result.db;
1896
+ boardService = result.boardService;
1897
+ taskService = new TaskService(db, boardService);
1898
+ } else {
1899
+ const { dbPath } = getKabanPaths(projectRoot);
1900
+ db = createDb2(dbPath);
1901
+ boardService = new BoardService2(db);
1902
+ taskService = new TaskService(db, boardService);
1903
+ }
1904
+ const board = await boardService.getBoard();
1905
+ if (!board) {
1906
+ console.error("Error: No board found.");
1907
+ renderer.destroy();
1908
+ process.exit(1);
1909
+ }
1910
+ const state = {
1911
+ renderer,
1912
+ taskService,
1913
+ boardService,
1914
+ boardName: board.name,
1915
+ columns: [],
1916
+ columnPanels: [],
1917
+ taskSelects: new Map,
1918
+ currentColumnIndex: 0,
1919
+ selectedTask: null,
1920
+ mainContainer: null,
1921
+ activeModal: "none",
1922
+ modalOverlay: null,
1923
+ taskInput: null,
1924
+ buttonRow: null,
1925
+ onModalConfirm: null,
1926
+ viewTaskState: null,
1927
+ editTaskState: null,
1928
+ viewTaskRuntime: null,
1929
+ editTaskRuntime: null
1930
+ };
1931
+ await refreshBoard(state);
1932
+ let lastDataVersion = null;
1933
+ const client = db.$client;
1934
+ const checkForChanges = async () => {
1935
+ if (state.activeModal !== "none")
1936
+ return;
1937
+ try {
1938
+ const result = await client.execute("PRAGMA data_version");
1939
+ const currentVersion = result.rows[0]?.[0];
1940
+ if (lastDataVersion !== null && currentVersion !== lastDataVersion) {
1941
+ await refreshBoard(state);
1942
+ }
1943
+ lastDataVersion = currentVersion;
1944
+ } catch {}
1945
+ };
1946
+ const pollInterval = setInterval(checkForChanges, POLL_INTERVAL_MS);
1947
+ const cleanup = () => clearInterval(pollInterval);
1948
+ process.on("exit", cleanup);
1949
+ process.on("SIGINT", cleanup);
1950
+ process.on("SIGTERM", cleanup);
1951
+ renderer.keyInput.on("keypress", (key) => {
1952
+ handleKeypress(state, key);
1953
+ });
1954
+ }
1955
+ main().catch((err) => {
1956
+ console.error("TUI Error:", err);
1957
+ process.exit(1);
1958
+ });