@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.
- package/dist/index.js +1958 -0
- 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
|
+
});
|