tuiboard 0.8.3 → 0.9.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/.tuiboard/config.example.yaml +9 -0
- package/CHANGELOG.md +102 -0
- package/README.md +111 -1
- package/bin/tuiboard.ts +12 -0
- package/package.json +5 -1
- package/src/app.tsx +70 -17
- package/src/boards/config-writer.ts +191 -0
- package/src/boards/create.ts +57 -0
- package/src/boards/scan.ts +95 -0
- package/src/boards/suggest.ts +34 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/board.ts +151 -0
- package/src/cli/summary.ts +284 -0
- package/src/cli/task.ts +235 -0
- package/src/config/loader.ts +60 -13
- package/src/input/handleKey.ts +72 -1
- package/src/io/watcher.ts +11 -0
- package/src/parser/markdown.ts +10 -0
- package/src/store/index.ts +379 -12
- package/src/ui/BoardView.tsx +8 -6
- package/src/ui/Chrome.tsx +65 -9
- package/src/ui/Modal.tsx +133 -2
- package/src/ui/PlannerPanel.tsx +1 -1
- package/src/ui/pane-ring.ts +73 -0
- package/src/views/BoardOnly.tsx +3 -2
- package/src/views/Dashboard.tsx +36 -8
- package/tsconfig.json +27 -0
- package/src/cli/args.test.ts +0 -40
- package/src/scripts/agents-check.ts +0 -24
- package/src/scripts/parse-check.ts +0 -124
- package/src/scripts/roundtrip-check.ts +0 -79
- package/src/store/agents.test.ts +0 -181
- package/src/store/index.test.ts +0 -224
- package/src/store/parsers.test.ts +0 -64
- package/src/store/timeline.test.ts +0 -332
- package/src/ui/board-scroll.test.ts +0 -63
- package/src/ui/glyphs.test.ts +0 -31
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { Board, Task } from "~/types";
|
|
4
|
-
import {
|
|
5
|
-
DAY_START_HOUR,
|
|
6
|
-
MINS_PER_ROW,
|
|
7
|
-
TOTAL_ROWS,
|
|
8
|
-
buildRowMap,
|
|
9
|
-
buildCalendarEntries,
|
|
10
|
-
buildTimelineEntries,
|
|
11
|
-
countOverlaps,
|
|
12
|
-
formatAgendaDay,
|
|
13
|
-
formatHm,
|
|
14
|
-
type TimelineEntry,
|
|
15
|
-
} from "./timeline";
|
|
16
|
-
|
|
17
|
-
/** Build a minimal Task object for testing. Only fields touched by timeline. */
|
|
18
|
-
function makeTask(opts: Partial<Task> & Pick<Task, "displayTitle">): Task {
|
|
19
|
-
const base: Partial<Task> = {
|
|
20
|
-
id: "0:0",
|
|
21
|
-
rawBody: opts.displayTitle,
|
|
22
|
-
rawLine: `- [ ] ${opts.displayTitle}`,
|
|
23
|
-
done: false,
|
|
24
|
-
priority: "none",
|
|
25
|
-
tags: [],
|
|
26
|
-
wikilinks: [],
|
|
27
|
-
dirty: false,
|
|
28
|
-
};
|
|
29
|
-
return { ...base, ...opts } as Task;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function makeBoard(name: string, filepath: string, tasks: Task[]): Board {
|
|
33
|
-
return {
|
|
34
|
-
name,
|
|
35
|
-
filepath,
|
|
36
|
-
frontmatter: "",
|
|
37
|
-
preamble: "",
|
|
38
|
-
trailer: "",
|
|
39
|
-
lineEnding: "\n",
|
|
40
|
-
originalContent: "",
|
|
41
|
-
columns: [
|
|
42
|
-
{
|
|
43
|
-
name: "Inbox",
|
|
44
|
-
headerLevel: 2,
|
|
45
|
-
rawHeading: "## Inbox",
|
|
46
|
-
children: tasks,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe("buildTimelineEntries", () => {
|
|
53
|
-
const today = "2026-05-27";
|
|
54
|
-
|
|
55
|
-
it("returns empty when no tasks are time-blocked today", () => {
|
|
56
|
-
const t = makeTask({ displayTitle: "no time block", scheduled: today });
|
|
57
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
58
|
-
expect(buildTimelineEntries([board], today)).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("includes a scheduled, time-blocked, non-done task for today", () => {
|
|
62
|
-
const t = makeTask({
|
|
63
|
-
displayTitle: "outreach",
|
|
64
|
-
scheduled: today,
|
|
65
|
-
timeBlock: { startMin: 9 * 60, endMin: 10 * 60 + 30 },
|
|
66
|
-
});
|
|
67
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
68
|
-
const entries = buildTimelineEntries([board], today);
|
|
69
|
-
expect(entries.length).toBe(1);
|
|
70
|
-
expect(entries[0]!.startMin).toBe(540);
|
|
71
|
-
expect(entries[0]!.endMin).toBe(630);
|
|
72
|
-
// 9:00 = row 8 (since DAY_START=7, 60min/15 = 4 rows per hour)
|
|
73
|
-
expect(entries[0]!.startRow).toBe(8);
|
|
74
|
-
// 90min duration = 6 rows
|
|
75
|
-
expect(entries[0]!.endRow).toBe(14);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("includes done tasks (timeline doubles as a done-log)", () => {
|
|
79
|
-
const t = makeTask({
|
|
80
|
-
displayTitle: "done deal",
|
|
81
|
-
done: true,
|
|
82
|
-
scheduled: today,
|
|
83
|
-
timeBlock: { startMin: 9 * 60, endMin: 10 * 60 },
|
|
84
|
-
});
|
|
85
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
86
|
-
const entries = buildTimelineEntries([board], today);
|
|
87
|
-
expect(entries).toHaveLength(1);
|
|
88
|
-
expect(entries[0]!.task.done).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("excludes tasks scheduled for a different date", () => {
|
|
92
|
-
const t = makeTask({
|
|
93
|
-
displayTitle: "tomorrow",
|
|
94
|
-
scheduled: "2026-05-28",
|
|
95
|
-
timeBlock: { startMin: 9 * 60, endMin: 10 * 60 },
|
|
96
|
-
});
|
|
97
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
98
|
-
expect(buildTimelineEntries([board], today)).toEqual([]);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("clips blocks that span outside the rendered window", () => {
|
|
102
|
-
const t = makeTask({
|
|
103
|
-
displayTitle: "early",
|
|
104
|
-
scheduled: today,
|
|
105
|
-
// 6:30-7:30 → only 7:00-7:30 visible (rows 0-1)
|
|
106
|
-
timeBlock: { startMin: 6 * 60 + 30, endMin: 7 * 60 + 30 },
|
|
107
|
-
});
|
|
108
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
109
|
-
const entries = buildTimelineEntries([board], today);
|
|
110
|
-
expect(entries.length).toBe(1);
|
|
111
|
-
expect(entries[0]!.startRow).toBe(0); // clipped at top
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("skips blocks entirely outside the window", () => {
|
|
115
|
-
const t = makeTask({
|
|
116
|
-
displayTitle: "midnight",
|
|
117
|
-
scheduled: today,
|
|
118
|
-
timeBlock: { startMin: 0, endMin: 30 },
|
|
119
|
-
});
|
|
120
|
-
const board = makeBoard("Work", "work.md", [t]);
|
|
121
|
-
expect(buildTimelineEntries([board], today)).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("sorts entries by startMin", () => {
|
|
125
|
-
const t1 = makeTask({
|
|
126
|
-
displayTitle: "afternoon",
|
|
127
|
-
scheduled: today,
|
|
128
|
-
timeBlock: { startMin: 14 * 60, endMin: 15 * 60 },
|
|
129
|
-
});
|
|
130
|
-
const t2 = makeTask({
|
|
131
|
-
displayTitle: "morning",
|
|
132
|
-
scheduled: today,
|
|
133
|
-
timeBlock: { startMin: 9 * 60, endMin: 10 * 60 },
|
|
134
|
-
});
|
|
135
|
-
const board = makeBoard("Work", "work.md", [t1, t2]);
|
|
136
|
-
const entries = buildTimelineEntries([board], today);
|
|
137
|
-
expect(entries.map((e) => e.task.displayTitle)).toEqual([
|
|
138
|
-
"morning",
|
|
139
|
-
"afternoon",
|
|
140
|
-
]);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("buildRowMap", () => {
|
|
145
|
-
function entryAt(startRow: number, endRow: number, title = "block"): TimelineEntry {
|
|
146
|
-
return {
|
|
147
|
-
kind: "task",
|
|
148
|
-
ref: { boardPath: "x", columnIndex: 0, taskIndex: 0 },
|
|
149
|
-
task: makeTask({ displayTitle: title }),
|
|
150
|
-
boardName: "Work",
|
|
151
|
-
boardIndex: 0,
|
|
152
|
-
columnName: "Inbox",
|
|
153
|
-
startMin: 0,
|
|
154
|
-
endMin: 0,
|
|
155
|
-
startRow,
|
|
156
|
-
endRow,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const titleOf = (e: TimelineEntry | undefined) =>
|
|
161
|
-
e?.kind === "task" ? e.task.displayTitle : undefined;
|
|
162
|
-
|
|
163
|
-
it("returns TOTAL_ROWS row pairs", () => {
|
|
164
|
-
const result = buildRowMap([], 0);
|
|
165
|
-
expect(result.rows.length).toBe(TOTAL_ROWS);
|
|
166
|
-
expect(result.overflow).toBe(0);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("marks hour-anchor rows on the left lane with kind=hour", () => {
|
|
170
|
-
const { rows } = buildRowMap([], 0);
|
|
171
|
-
expect(rows[0]!.left).toEqual({ kind: "hour", hour: DAY_START_HOUR });
|
|
172
|
-
expect(rows[4]!.left).toEqual({ kind: "hour", hour: DAY_START_HOUR + 1 });
|
|
173
|
-
expect(rows[1]!.left.kind).toBe("empty");
|
|
174
|
-
// Right lane is always empty when no overlap.
|
|
175
|
-
expect(rows[0]!.right.kind).toBe("empty");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("places a single entry on the left lane as head/body/fill", () => {
|
|
179
|
-
const { rows } = buildRowMap([entryAt(8, 14)], 0);
|
|
180
|
-
expect(rows[8]!.left.kind).toBe("head");
|
|
181
|
-
expect(rows[9]!.left.kind).toBe("body");
|
|
182
|
-
expect(rows[10]!.left.kind).toBe("fill");
|
|
183
|
-
expect(rows[13]!.left.kind).toBe("fill");
|
|
184
|
-
expect(rows[14]!.left.kind).not.toBe("fill"); // end exclusive
|
|
185
|
-
// No overlap → right lane all empty.
|
|
186
|
-
for (let r = 8; r < 14; r++) {
|
|
187
|
-
expect(rows[r]!.right.kind).toBe("empty");
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("places overlapping entries on left and right lanes side-by-side", () => {
|
|
192
|
-
// A: rows 5-10, B: rows 7-12 → overlap on rows 7-9.
|
|
193
|
-
const a = entryAt(5, 10, "A");
|
|
194
|
-
const b = entryAt(7, 12, "B");
|
|
195
|
-
const { rows, overflow } = buildRowMap([a, b], 0);
|
|
196
|
-
expect(overflow).toBe(0);
|
|
197
|
-
// Lane 0 (left) gets A.
|
|
198
|
-
expect(rows[5]!.left.kind).toBe("head");
|
|
199
|
-
expect(titleOf(rows[5]!.left.entry)).toBe("A");
|
|
200
|
-
// Lane 1 (right) gets B starting at row 7.
|
|
201
|
-
expect(rows[7]!.right.kind).toBe("head");
|
|
202
|
-
expect(titleOf(rows[7]!.right.entry)).toBe("B");
|
|
203
|
-
// Row 10 is past A's end but inside B → left empty, right fill.
|
|
204
|
-
expect(rows[10]!.left.kind).toBe("empty");
|
|
205
|
-
expect(titleOf(rows[10]!.right.entry)).toBe("B");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("counts third+ overlapping entry as overflow", () => {
|
|
209
|
-
// Three blocks overlapping at row 8.
|
|
210
|
-
const a = entryAt(5, 12, "A");
|
|
211
|
-
const b = entryAt(6, 11, "B");
|
|
212
|
-
const c = entryAt(7, 10, "C");
|
|
213
|
-
const { overflow } = buildRowMap([a, b, c], 0);
|
|
214
|
-
expect(overflow).toBe(1); // C didn't fit either lane
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("reuses a lane after its block ends", () => {
|
|
218
|
-
// A: rows 5-8, C: rows 10-15 → both can use lane 0.
|
|
219
|
-
const a = entryAt(5, 8, "A");
|
|
220
|
-
const c = entryAt(10, 15, "C");
|
|
221
|
-
const { rows, overflow } = buildRowMap([a, c], 0);
|
|
222
|
-
expect(overflow).toBe(0);
|
|
223
|
-
expect(titleOf(rows[5]!.left.entry)).toBe("A");
|
|
224
|
-
expect(titleOf(rows[10]!.left.entry)).toBe("C");
|
|
225
|
-
// Right lane stayed empty throughout.
|
|
226
|
-
for (let r = 0; r < TOTAL_ROWS; r++) {
|
|
227
|
-
expect(rows[r]!.right.kind).toBe("empty");
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("overlays a now marker on the left lane (and clears the right)", () => {
|
|
232
|
-
const nowMin = 10 * 60 + 30; // 10:30 → row 14
|
|
233
|
-
const { rows } = buildRowMap([], nowMin);
|
|
234
|
-
expect(rows[14]!.left.kind).toBe("now");
|
|
235
|
-
expect(rows[14]!.left.nowMin).toBe(nowMin);
|
|
236
|
-
expect(rows[14]!.right.kind).toBe("empty");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("does not place a now marker when out of window", () => {
|
|
240
|
-
const { rows } = buildRowMap([], 3 * 60); // 03:00, before DAY_START
|
|
241
|
-
expect(rows.some((r) => r.left.kind === "now")).toBe(false);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("countOverlaps", () => {
|
|
246
|
-
function entry(s: number, e: number): TimelineEntry {
|
|
247
|
-
return {
|
|
248
|
-
kind: "task",
|
|
249
|
-
ref: { boardPath: "x", columnIndex: 0, taskIndex: 0 },
|
|
250
|
-
task: makeTask({ displayTitle: "x" }),
|
|
251
|
-
boardName: "X",
|
|
252
|
-
boardIndex: 0,
|
|
253
|
-
columnName: "X",
|
|
254
|
-
startMin: s,
|
|
255
|
-
endMin: e,
|
|
256
|
-
startRow: 0,
|
|
257
|
-
endRow: 0,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
it("returns 0 for non-overlapping blocks", () => {
|
|
262
|
-
expect(
|
|
263
|
-
countOverlaps([entry(540, 600), entry(600, 660), entry(720, 780)]),
|
|
264
|
-
).toBe(0);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("counts overlapping pairs", () => {
|
|
268
|
-
expect(countOverlaps([entry(540, 660), entry(600, 720)])).toBe(1);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("counts every overlapping pair in a 3-way overlap", () => {
|
|
272
|
-
expect(
|
|
273
|
-
countOverlaps([entry(540, 660), entry(600, 720), entry(630, 700)]),
|
|
274
|
-
).toBe(3);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
describe("formatAgendaDay", () => {
|
|
279
|
-
it("uses relative words for the near days", () => {
|
|
280
|
-
expect(formatAgendaDay(0, "2026-05-30")).toBe("Today");
|
|
281
|
-
expect(formatAgendaDay(1, "2026-05-31")).toBe("Tomorrow");
|
|
282
|
-
expect(formatAgendaDay(-1, "2026-05-29")).toBe("Yesterday");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("stamps weekday + day + month for farther days", () => {
|
|
286
|
-
// 2026-06-02 is a Tuesday.
|
|
287
|
-
expect(formatAgendaDay(3, "2026-06-02")).toBe("Tue 02 Jun");
|
|
288
|
-
// 2026-05-25 is a Monday.
|
|
289
|
-
expect(formatAgendaDay(-5, "2026-05-25")).toBe("Mon 25 May");
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("buildCalendarEntries", () => {
|
|
294
|
-
const ev = (startMin: number, endMin: number, title = "Standup") => ({
|
|
295
|
-
title,
|
|
296
|
-
startMin,
|
|
297
|
-
endMin,
|
|
298
|
-
color: "#FF5F00",
|
|
299
|
-
source: "google" as const,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("maps an in-window event to a calendar entry with rows", () => {
|
|
303
|
-
const out = buildCalendarEntries([ev(9 * 60, 10 * 60, "Standup")]);
|
|
304
|
-
expect(out).toHaveLength(1);
|
|
305
|
-
expect(out[0]!.kind).toBe("calendar");
|
|
306
|
-
expect(out[0]!.title).toBe("Standup");
|
|
307
|
-
expect(out[0]!.color).toBe("#FF5F00");
|
|
308
|
-
// 09:00 → (540 - DAY_START*60) / 15
|
|
309
|
-
expect(out[0]!.startRow).toBe((9 * 60 - DAY_START_HOUR * 60) / MINS_PER_ROW);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("drops events entirely outside the rendered window", () => {
|
|
313
|
-
expect(buildCalendarEntries([ev(2 * 60, 3 * 60)])).toEqual([]);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("sorts by start time", () => {
|
|
317
|
-
const out = buildCalendarEntries([ev(11 * 60, 12 * 60, "late"), ev(8 * 60, 9 * 60, "early")]);
|
|
318
|
-
expect(out.map((e) => e.title)).toEqual(["early", "late"]);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe("formatHm", () => {
|
|
323
|
-
it("zero-pads hours and minutes", () => {
|
|
324
|
-
expect(formatHm(0)).toBe("00:00");
|
|
325
|
-
expect(formatHm(540)).toBe("09:00");
|
|
326
|
-
expect(formatHm(615)).toBe("10:15");
|
|
327
|
-
expect(formatHm(23 * 60 + 59)).toBe("23:59");
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// silence unused-var lint about MINS_PER_ROW import
|
|
332
|
-
void MINS_PER_ROW;
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { computeColumnScrollLeft } from "~/ui/board-scroll";
|
|
4
|
-
|
|
5
|
-
const COL = 42;
|
|
6
|
-
const GAP = 1;
|
|
7
|
-
const STRIDE = COL + GAP;
|
|
8
|
-
|
|
9
|
-
// Helper for the common uniform-width case: column at a given index.
|
|
10
|
-
function scroll(index: number, viewportWidth: number, currentScroll: number) {
|
|
11
|
-
return computeColumnScrollLeft({
|
|
12
|
-
colStart: index * STRIDE,
|
|
13
|
-
colWidth: COL,
|
|
14
|
-
viewportWidth,
|
|
15
|
-
currentScroll,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("computeColumnScrollLeft", () => {
|
|
20
|
-
test("column already fully visible → scroll unchanged", () => {
|
|
21
|
-
expect(scroll(0, 100, 0)).toBe(0);
|
|
22
|
-
expect(scroll(1, 100, 0)).toBe(0);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("column off the right edge → align its right edge to viewport", () => {
|
|
26
|
-
// viewport 60, col 1 spans 43..85 → right-align: 85-60 = 25.
|
|
27
|
-
expect(scroll(1, 60, 0)).toBe(25);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("column off the left edge → align its left edge to viewport", () => {
|
|
31
|
-
expect(scroll(0, 60, 25)).toBe(0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("far-right hidden column scrolls fully into view", () => {
|
|
35
|
-
// col 5 spans 215..257 → right-align: 257-60 = 197.
|
|
36
|
-
expect(scroll(5, 60, 0)).toBe(197);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("column wider than viewport → align left edge (show the start)", () => {
|
|
40
|
-
// viewport 30 < column 42. col 2 starts at 86 → align left edge at 86.
|
|
41
|
-
expect(scroll(2, 30, 0)).toBe(86);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("negative start or zero viewport → no change", () => {
|
|
45
|
-
expect(
|
|
46
|
-
computeColumnScrollLeft({ colStart: -1, colWidth: COL, viewportWidth: 60, currentScroll: 17 }),
|
|
47
|
-
).toBe(17);
|
|
48
|
-
expect(scroll(3, 0, 17)).toBe(17);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("never returns a negative scroll offset", () => {
|
|
52
|
-
expect(scroll(0, 200, 0)).toBeGreaterThanOrEqual(0);
|
|
53
|
-
expect(scroll(0, 30, 5)).toBeGreaterThanOrEqual(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("variable widths: narrow collapsed column to the left shifts offsets", () => {
|
|
57
|
-
// Columns: [42], [18 collapsed], [42]. Third column starts at
|
|
58
|
-
// 43 + 19 = 62, spans 62..104. viewport 50 → right-align 104-50 = 54.
|
|
59
|
-
expect(
|
|
60
|
-
computeColumnScrollLeft({ colStart: 62, colWidth: 42, viewportWidth: 50, currentScroll: 0 }),
|
|
61
|
-
).toBe(54);
|
|
62
|
-
});
|
|
63
|
-
});
|
package/src/ui/glyphs.test.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { cellWidth } from "~/ui/glyphs";
|
|
4
|
-
|
|
5
|
-
describe("cellWidth", () => {
|
|
6
|
-
test("ASCII counts one cell per char", () => {
|
|
7
|
-
expect(cellWidth("09:00")).toBe(5);
|
|
8
|
-
expect(cellWidth("Costruire routine")).toBe(17);
|
|
9
|
-
expect(cellWidth("")).toBe(0);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("the ⌚ time-block glyph is 2 cells (the bug that broke truncation)", () => {
|
|
13
|
-
expect(cellWidth("⌚")).toBe(2);
|
|
14
|
-
// The actual suffix on a time-blocked row: ⌚ + "09:00" = 2 + 5 = 7,
|
|
15
|
-
// not the 6 that String.length reports.
|
|
16
|
-
expect(cellWidth("⌚09:00")).toBe(7);
|
|
17
|
-
expect("⌚09:00".length).toBe(6); // proves the undercount we corrected
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("priority + clock emoji are 2 cells", () => {
|
|
21
|
-
expect(cellWidth("🔺")).toBe(2); // U+1F53A
|
|
22
|
-
expect(cellWidth("⏰")).toBe(2); // U+23F0
|
|
23
|
-
expect(cellWidth("⏫")).toBe(2); // U+23EB
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("narrow symbols used in rows stay 1 cell", () => {
|
|
27
|
-
expect(cellWidth("✓")).toBe(1); // done check
|
|
28
|
-
expect(cellWidth("●")).toBe(1); // marked dot
|
|
29
|
-
expect(cellWidth("→")).toBe(1); // tomorrow arrow
|
|
30
|
-
});
|
|
31
|
-
});
|