tuiboard 0.8.0 → 0.8.1
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 +3 -2
- package/CHANGELOG.md +19 -0
- package/README.md +26 -10
- package/package.json +1 -1
- package/src/store/calendar.ts +56 -8
- package/src/store/index.ts +12 -5
- package/src/ui/Modal.tsx +77 -37
- package/src/ui/TimelineView.tsx +32 -5
|
@@ -65,8 +65,9 @@ archive_column: Archive
|
|
|
65
65
|
# Optional: overlay read-only calendar events on the Agenda (the 24h timeline).
|
|
66
66
|
# Connect a provider with `tuiboard calendar-setup google` / `... microsoft`,
|
|
67
67
|
# which opens the auth flow and prints the exact block to paste here. Bring your
|
|
68
|
-
# own credentials — nothing is hosted, nothing leaves your machine.
|
|
69
|
-
#
|
|
68
|
+
# own credentials — nothing is hosted, nothing leaves your machine. Timed events
|
|
69
|
+
# render on the 24h grid; all-day events show as chips at the top. Each calendar
|
|
70
|
+
# keeps its own color. Paths support `~`.
|
|
70
71
|
#
|
|
71
72
|
# To CREATE / EDIT / DELETE events (not just read), re-auth with:
|
|
72
73
|
# calendar-setup google --write. Then press `n` / click an empty slot to create;
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to **tuiboard** are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.1] - 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Set an event's date from the Agenda modal.** When creating or editing a
|
|
12
|
+
Google Calendar event you can now append a date token to the title — `t` /
|
|
13
|
+
`tm` / `+3` / `lun` / `2026-06-10`, the same shortcuts as task scheduling — so
|
|
14
|
+
you're no longer limited to the day the Agenda is showing. Natural order is
|
|
15
|
+
`Title [date] HH:MM-HH:MM` (e.g. `Lunch tomorrow 12-13`); without a date token
|
|
16
|
+
it still defaults to the viewed day. On edit, a date token moves the event to
|
|
17
|
+
another day.
|
|
18
|
+
- **All-day events now show in the Agenda.** Previously skipped, all-day events
|
|
19
|
+
(Google and Microsoft) render as a chip strip at the top of the Agenda — like
|
|
20
|
+
Google Calendar's all-day band — instead of being dropped.
|
|
21
|
+
- **Create all-day events from the Agenda.** Add `allday` (or `all-day`) anywhere
|
|
22
|
+
in the new-event title — e.g. `Holiday 2026-12-25 allday` — to create a
|
|
23
|
+
date-only Google event instead of a timed one. It appears in the top chip
|
|
24
|
+
strip. (Editing all-day events isn't supported; create only.)
|
|
25
|
+
|
|
8
26
|
## [0.8.0] - 2026-06-03
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -134,6 +152,7 @@ First public release on npm. This entry captures the full feature set at launch.
|
|
|
134
152
|
|
|
135
153
|
Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
|
|
136
154
|
|
|
155
|
+
[0.8.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.1
|
|
137
156
|
[0.8.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.8.0
|
|
138
157
|
[0.7.3]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.3
|
|
139
158
|
[0.7.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.7.2
|
package/README.md
CHANGED
|
@@ -179,12 +179,14 @@ at all — handy if you don't use Claude Code and don't want tuiboard reading
|
|
|
179
179
|
|
|
180
180
|
## Calendars (Agenda overlay)
|
|
181
181
|
|
|
182
|
-
The **Agenda** zone (the 24h timeline) can overlay
|
|
183
|
-
|
|
184
|
-
colored `📅` blocks
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
credentials** — there's nothing to sign
|
|
182
|
+
The **Agenda** zone (the 24h timeline) can overlay events from Google Calendar
|
|
183
|
+
and Microsoft 365 alongside your time-blocked tasks — timed events render as
|
|
184
|
+
colored `📅` blocks on the grid, and all-day events ride in a chip strip at the
|
|
185
|
+
top (like Google Calendar's all-day band). The day's real shape is visible at a
|
|
186
|
+
glance. Each calendar keeps its own color. Events are cached 30 min on disk and
|
|
187
|
+
refreshed every 5 min. **Bring your own credentials** — there's nothing to sign
|
|
188
|
+
up for and nothing leaves your machine. (Reading is the default; opt into
|
|
189
|
+
creating/editing events below.)
|
|
188
190
|
|
|
189
191
|
Connect a calendar with the built-in setup command:
|
|
190
192
|
|
|
@@ -253,15 +255,29 @@ tuiboard calendar-setup google --write
|
|
|
253
255
|
```
|
|
254
256
|
|
|
255
257
|
**Create** — in the Agenda zone, press **`n`** (or **click an empty time slot**)
|
|
256
|
-
to open the new-event modal: type a title
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
to open the new-event modal: type a title, press Enter, pick the target calendar
|
|
259
|
+
with `j`/`k`, Enter to create. Only calendars you can write to (owner/writer)
|
|
260
|
+
show in the picker. Append tokens to the title to set the **time** and **date**:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
Standup 9:00-9:30 # today (or the viewed day), 09:00–09:30
|
|
264
|
+
Lunch tomorrow 12-13 # tomorrow, 12:00–13:00
|
|
265
|
+
Review 2026-06-10 15-16 # that date, 15:00–16:00
|
|
266
|
+
Call +3 16:00-16:30 # in 3 days · lun = next Monday also works
|
|
267
|
+
Holiday 2026-12-25 allday # an all-day event (no time)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The date defaults to whichever day the Agenda is showing; an explicit date token
|
|
271
|
+
(`t` / `tm` / `+N` / weekday / `YYYY-MM-DD`) overrides it. The time is taken from
|
|
272
|
+
the clicked slot, or `HH:MM-HH:MM`. Add **`allday`** (or `all-day`) anywhere in
|
|
273
|
+
the title to create an all-day event instead — it lands in the top chip strip.
|
|
259
274
|
|
|
260
275
|
**Edit / delete** — **click an existing event** in the Agenda to select it (only
|
|
261
276
|
events on writable calendars can be selected; read-only ones just say so). Then:
|
|
262
277
|
|
|
263
278
|
- **`e`** (or Enter) opens the edit modal, prefilled with the title and time —
|
|
264
|
-
change
|
|
279
|
+
change the title, time, and/or date (same `Title [date] HH:MM-HH:MM` syntax)
|
|
280
|
+
and Enter to save. A date token moves the event to another day.
|
|
265
281
|
- **`d`** deletes it (with a confirm).
|
|
266
282
|
- **`Esc`** deselects.
|
|
267
283
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tuiboard",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/store/calendar.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface CalEvent {
|
|
|
37
37
|
* on an owner/writer calendar, with a write-scoped token. Microsoft events
|
|
38
38
|
* and read-only-calendar events are never editable. */
|
|
39
39
|
editable?: boolean;
|
|
40
|
+
/** All-day event (date-only, no time). Rendered as a chip at the top of the
|
|
41
|
+
* Agenda rather than on the 24h grid; startMin/endMin are unused. Display
|
|
42
|
+
* only — not editable from tuiboard. */
|
|
43
|
+
allDay?: boolean;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const GOOGLE_FALLBACK_COLOR = "#e8a05c";
|
|
@@ -231,29 +235,45 @@ function localRfc3339(dateIso: string, min: number): string {
|
|
|
231
235
|
);
|
|
232
236
|
}
|
|
233
237
|
|
|
238
|
+
/** The day after `dateIso` (YYYY-MM-DD). Google all-day events use an EXCLUSIVE
|
|
239
|
+
* end date, so a single-day all-day event ends on the following day. */
|
|
240
|
+
function nextDayIso(dateIso: string): string {
|
|
241
|
+
const d = new Date(dayStartMs(dateIso) + 24 * 60 * 60000);
|
|
242
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
243
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
/**
|
|
235
247
|
* Create a Google Calendar event on `calendarId`. Unlike the read path, this
|
|
236
248
|
* surfaces failures (returns {ok:false,error}) so the UI can flash a banner.
|
|
249
|
+
* Pass `allDay` for a date-only event (start/end as dates, end exclusive).
|
|
237
250
|
*/
|
|
238
251
|
export async function createGoogleEvent(
|
|
239
252
|
cfg: GoogleCalendarConfig,
|
|
240
|
-
args: { calendarId: string; title: string; dateIso: string; startMin: number; endMin: number },
|
|
253
|
+
args: { calendarId: string; title: string; dateIso: string; startMin: number; endMin: number; allDay?: boolean },
|
|
241
254
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
242
255
|
const access = await googleAccessToken(cfg.token);
|
|
243
256
|
if (!access) {
|
|
244
257
|
return { ok: false, error: "not authorized — run: tuiboard calendar-setup google --write" };
|
|
245
258
|
}
|
|
246
259
|
try {
|
|
260
|
+
const body = args.allDay
|
|
261
|
+
? {
|
|
262
|
+
summary: args.title,
|
|
263
|
+
start: { date: args.dateIso },
|
|
264
|
+
end: { date: nextDayIso(args.dateIso) },
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
summary: args.title,
|
|
268
|
+
start: { dateTime: localRfc3339(args.dateIso, args.startMin) },
|
|
269
|
+
end: { dateTime: localRfc3339(args.dateIso, args.endMin) },
|
|
270
|
+
};
|
|
247
271
|
const res = await fetch(
|
|
248
272
|
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(args.calendarId)}/events`,
|
|
249
273
|
{
|
|
250
274
|
method: "POST",
|
|
251
275
|
headers: { Authorization: `Bearer ${access}`, "Content-Type": "application/json" },
|
|
252
|
-
body: JSON.stringify(
|
|
253
|
-
summary: args.title,
|
|
254
|
-
start: { dateTime: localRfc3339(args.dateIso, args.startMin) },
|
|
255
|
-
end: { dateTime: localRfc3339(args.dateIso, args.endMin) },
|
|
256
|
-
}),
|
|
276
|
+
body: JSON.stringify(body),
|
|
257
277
|
},
|
|
258
278
|
);
|
|
259
279
|
if (!res.ok) {
|
|
@@ -387,7 +407,24 @@ async function fetchGoogle(
|
|
|
387
407
|
for (const it of data.items ?? []) {
|
|
388
408
|
const startRaw = it.start?.dateTime;
|
|
389
409
|
const endRaw = it.end?.dateTime;
|
|
390
|
-
if (!startRaw || !endRaw)
|
|
410
|
+
if (!startRaw || !endRaw) {
|
|
411
|
+
// All-day / date-only event: the per-day query already scoped it
|
|
412
|
+
// to a day it covers, so just surface it as a chip (display only).
|
|
413
|
+
const dayKey = it.start?.date;
|
|
414
|
+
if (!dayKey) continue;
|
|
415
|
+
out.push({
|
|
416
|
+
uid: it.id ?? `${cal.id}:allday:${dayKey}`,
|
|
417
|
+
ev: {
|
|
418
|
+
title: it.summary ?? "(no title)",
|
|
419
|
+
startMin: 0,
|
|
420
|
+
endMin: 0,
|
|
421
|
+
color: cal.color,
|
|
422
|
+
source: "google",
|
|
423
|
+
allDay: true,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
391
428
|
const { startMin, endMin } = toMinutes(Date.parse(startRaw), Date.parse(endRaw), base);
|
|
392
429
|
out.push({
|
|
393
430
|
uid: it.id ?? `${cal.id}:${startRaw}`,
|
|
@@ -540,7 +577,18 @@ async function fetchMicrosoft(
|
|
|
540
577
|
};
|
|
541
578
|
const events: CalEvent[] = [];
|
|
542
579
|
for (const it of data.value ?? []) {
|
|
543
|
-
if (it.isAllDay)
|
|
580
|
+
if (it.isAllDay) {
|
|
581
|
+
// All-day event → chip at the top of the Agenda (display only).
|
|
582
|
+
events.push({
|
|
583
|
+
title: it.subject ?? "(no title)",
|
|
584
|
+
startMin: 0,
|
|
585
|
+
endMin: 0,
|
|
586
|
+
color,
|
|
587
|
+
source: "microsoft",
|
|
588
|
+
allDay: true,
|
|
589
|
+
});
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
544
592
|
const s = it.start?.dateTime;
|
|
545
593
|
const e = it.end?.dateTime;
|
|
546
594
|
if (!s || !e) continue;
|
package/src/store/index.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface EventPicker {
|
|
|
95
95
|
dateIso: string;
|
|
96
96
|
startMin: number;
|
|
97
97
|
endMin: number;
|
|
98
|
+
/** Create as a date-only all-day event (start/end times ignored). */
|
|
99
|
+
allDay?: boolean;
|
|
98
100
|
cals: WritableCalendar[];
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -1137,7 +1139,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1137
1139
|
/** Step 1 → step 2: stash the parsed title + time, load calendars if needed,
|
|
1138
1140
|
* preselect the default, and either show the picker or (single calendar)
|
|
1139
1141
|
* create immediately. */
|
|
1140
|
-
async function advanceEventToStep2(title: string, startMin: number, endMin: number): Promise<void> {
|
|
1142
|
+
async function advanceEventToStep2(title: string, startMin: number, endMin: number, dateIso?: string, allDay?: boolean): Promise<void> {
|
|
1141
1143
|
const g = config.calendars?.google;
|
|
1142
1144
|
if (!g || !state.ui.eventPicker) { closeModal(); return; }
|
|
1143
1145
|
let cals = state.ui.eventPicker.cals;
|
|
@@ -1154,6 +1156,8 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1154
1156
|
p.title = title;
|
|
1155
1157
|
p.startMin = startMin;
|
|
1156
1158
|
p.endMin = endMin;
|
|
1159
|
+
if (dateIso) p.dateIso = dateIso; // explicit date token overrides the viewed day
|
|
1160
|
+
p.allDay = !!allDay;
|
|
1157
1161
|
p.cals = cals;
|
|
1158
1162
|
p.sel = idx < 0 ? 0 : idx;
|
|
1159
1163
|
p.step = 2;
|
|
@@ -1176,6 +1180,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1176
1180
|
if (!p || !g) { closeModal(); return; }
|
|
1177
1181
|
const calendarId = p.cals[p.sel]?.id ?? g.defaultCalendar ?? "primary";
|
|
1178
1182
|
const title = p.title.trim() || "(busy)";
|
|
1183
|
+
const allDay = !!p.allDay;
|
|
1179
1184
|
closeModal();
|
|
1180
1185
|
const r = await createGoogleEvent(g, {
|
|
1181
1186
|
calendarId,
|
|
@@ -1183,10 +1188,11 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1183
1188
|
dateIso: p.dateIso,
|
|
1184
1189
|
startMin: p.startMin,
|
|
1185
1190
|
endMin: p.endMin,
|
|
1191
|
+
allDay,
|
|
1186
1192
|
});
|
|
1187
1193
|
if (r.ok) {
|
|
1188
1194
|
calendarStore.refresh(true);
|
|
1189
|
-
flashBanner("info", `📅 Event created: ${title}`);
|
|
1195
|
+
flashBanner("info", allDay ? `📅 All-day event created: ${title}` : `📅 Event created: ${title}`);
|
|
1190
1196
|
} else {
|
|
1191
1197
|
flashBanner("error", `Create failed: ${r.error}`);
|
|
1192
1198
|
}
|
|
@@ -1218,8 +1224,9 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1218
1224
|
setTimeout(() => openModal({ kind: "event-edit" }), 0);
|
|
1219
1225
|
}
|
|
1220
1226
|
|
|
1221
|
-
/** Save the edited title/time to the selected event, refresh, close.
|
|
1222
|
-
|
|
1227
|
+
/** Save the edited title/time/date to the selected event, refresh, close.
|
|
1228
|
+
* `dateIso` (optional) moves the event to another day; defaults to its day. */
|
|
1229
|
+
async function confirmEventEdit(title: string, startMin: number, endMin: number, dateIso?: string): Promise<void> {
|
|
1223
1230
|
const sel = state.ui.selectedCalEvent;
|
|
1224
1231
|
const g = config.calendars?.google;
|
|
1225
1232
|
if (!sel || !g) { closeModal(); return; }
|
|
@@ -1228,7 +1235,7 @@ export function createTuiStore({ config }: CreateStoreOptions) {
|
|
|
1228
1235
|
calendarId: sel.calendarId,
|
|
1229
1236
|
eventId: sel.eventId,
|
|
1230
1237
|
title: title.trim() || sel.title,
|
|
1231
|
-
dateIso: sel.dateIso,
|
|
1238
|
+
dateIso: dateIso ?? sel.dateIso,
|
|
1232
1239
|
startMin,
|
|
1233
1240
|
endMin,
|
|
1234
1241
|
});
|
package/src/ui/Modal.tsx
CHANGED
|
@@ -273,9 +273,76 @@ function TimeBlockModal(props: { store: TuiStore; modal: Extract<NonNullable<Tui
|
|
|
273
273
|
|
|
274
274
|
// ─── New calendar event ──────────────────────────────────────────────────────
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Parse an event input line into title + time + (optional) date. Tokens are
|
|
278
|
+
* peeled off the END, time first then date, so the natural order is
|
|
279
|
+
* "Title [date] [time]":
|
|
280
|
+
* "Standup 9:00-9:30" → title, 09:00-09:30, (default date)
|
|
281
|
+
* "Lunch tomorrow 12-13" → title, 12:00-13:00, tomorrow
|
|
282
|
+
* "Review 2026-06-10 15-16" → title, 15:00-16:00, that date
|
|
283
|
+
* A trailing token is only consumed if it parses AND something is left for the
|
|
284
|
+
* title, so a one-word title like "tomorrow" stays a title. `dateIso` is set
|
|
285
|
+
* only when an explicit date token was found (caller falls back to its default).
|
|
286
|
+
*/
|
|
287
|
+
function peelEventInput(
|
|
288
|
+
text: string,
|
|
289
|
+
defStart: number,
|
|
290
|
+
defEnd: number,
|
|
291
|
+
): { title: string; startMin: number; endMin: number; dateIso?: string; allDay: boolean } {
|
|
292
|
+
let title = text.trim();
|
|
293
|
+
let startMin = defStart;
|
|
294
|
+
let endMin = defEnd;
|
|
295
|
+
let dateIso: string | undefined;
|
|
296
|
+
|
|
297
|
+
// 0) an "allday" / "all-day" keyword anywhere → date-only event (time ignored)
|
|
298
|
+
let allDay = false;
|
|
299
|
+
if (/(^|\s)(all-?day)(\s|$)/i.test(title)) {
|
|
300
|
+
allDay = true;
|
|
301
|
+
title = title.replace(/(^|\s)all-?day(\s|$)/i, " ").replace(/\s+/g, " ").trim();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 1) trailing time token
|
|
305
|
+
const mt = title.match(/\s(\S+)$/);
|
|
306
|
+
const ttok = mt?.[1];
|
|
307
|
+
if (mt && mt.index !== undefined && ttok) {
|
|
308
|
+
const tb = parseTimeBlockShortcut(ttok);
|
|
309
|
+
if (tb && tb.endMin > tb.startMin) {
|
|
310
|
+
const rest = title.slice(0, mt.index).trim();
|
|
311
|
+
if (rest) {
|
|
312
|
+
title = rest;
|
|
313
|
+
startMin = tb.startMin;
|
|
314
|
+
endMin = tb.endMin;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// 2) trailing date token (on what's left)
|
|
319
|
+
const md = title.match(/\s(\S+)$/);
|
|
320
|
+
const dtok = md?.[1];
|
|
321
|
+
if (md && md.index !== undefined && dtok) {
|
|
322
|
+
const d = parseDateShortcut(dtok);
|
|
323
|
+
if (typeof d === "string") {
|
|
324
|
+
const rest = title.slice(0, md.index).trim();
|
|
325
|
+
if (rest) {
|
|
326
|
+
title = rest;
|
|
327
|
+
dateIso = d;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { title, startMin, endMin, dateIso, allDay };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Short "Mon 10 Jun" style label for an ISO date, for modal titles. */
|
|
335
|
+
function shortDate(iso: string): string {
|
|
336
|
+
const [y, m, d] = iso.split("-").map((n) => parseInt(n, 10));
|
|
337
|
+
if (!y || !m || !d) return iso;
|
|
338
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
339
|
+
return `${d} ${months[m - 1]}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
276
342
|
/**
|
|
277
343
|
* Two-step "new Google Calendar event" modal. Step 1: a title+time `<input>`
|
|
278
|
-
* (time prefilled from the clicked slot; append `HH:MM-HH:MM` to override
|
|
344
|
+
* (time prefilled from the clicked slot; append `HH:MM-HH:MM` to override, and
|
|
345
|
+
* a date token like `tomorrow` / `2026-06-10` to change the day).
|
|
279
346
|
* Step 2: a non-input calendar list navigated via handleKey (no input focused),
|
|
280
347
|
* preselected to the configured default. See `openEventModal` / `confirmEventPicker`.
|
|
281
348
|
*/
|
|
@@ -288,26 +355,12 @@ function EventModal(props: { store: TuiStore }) {
|
|
|
288
355
|
function submit(text: string) {
|
|
289
356
|
const p = picker();
|
|
290
357
|
if (!p) return;
|
|
291
|
-
const
|
|
292
|
-
let title = trimmed;
|
|
293
|
-
let startMin = p.startMin;
|
|
294
|
-
let endMin = p.endMin;
|
|
295
|
-
// Peel a trailing time token: "Standup 9:00-9:30" / "Lunch 12-13".
|
|
296
|
-
const m = trimmed.match(/\s(\S+)$/);
|
|
297
|
-
const tok = m?.[1];
|
|
298
|
-
if (m && m.index !== undefined && tok) {
|
|
299
|
-
const tb = parseTimeBlockShortcut(tok);
|
|
300
|
-
if (tb && tb.endMin > tb.startMin) {
|
|
301
|
-
title = trimmed.slice(0, m.index).trim();
|
|
302
|
-
startMin = tb.startMin;
|
|
303
|
-
endMin = tb.endMin;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
358
|
+
const { title, startMin, endMin, dateIso, allDay } = peelEventInput(text, p.startMin, p.endMin);
|
|
306
359
|
if (!title) {
|
|
307
360
|
setError("Title required");
|
|
308
361
|
return;
|
|
309
362
|
}
|
|
310
|
-
void props.store.advanceEventToStep2(title, startMin, endMin);
|
|
363
|
+
void props.store.advanceEventToStep2(title, startMin, endMin, dateIso, allDay);
|
|
311
364
|
}
|
|
312
365
|
|
|
313
366
|
return (
|
|
@@ -317,7 +370,7 @@ function EventModal(props: { store: TuiStore }) {
|
|
|
317
370
|
fallback={
|
|
318
371
|
<DialogShell
|
|
319
372
|
title="New event"
|
|
320
|
-
hint={`${formatHm(picker()!.startMin)}-${formatHm(picker()!.endMin)} ·
|
|
373
|
+
hint={`${formatHm(picker()!.startMin)}-${formatHm(picker()!.endMin)} · add a time, a date (tm · +3 · 2026-06-10), or "allday" · Enter add · Esc`}
|
|
321
374
|
>
|
|
322
375
|
<input
|
|
323
376
|
focused
|
|
@@ -337,7 +390,7 @@ function EventModal(props: { store: TuiStore }) {
|
|
|
337
390
|
}
|
|
338
391
|
>
|
|
339
392
|
<DialogShell
|
|
340
|
-
title={`Calendar · ${formatHm(picker()!.startMin)}-${formatHm(picker()!.endMin)}`}
|
|
393
|
+
title={`Calendar · ${shortDate(picker()!.dateIso)} ${picker()!.allDay ? "all day" : `${formatHm(picker()!.startMin)}-${formatHm(picker()!.endMin)}`}`}
|
|
341
394
|
hint="j/k choose · Enter create · Esc cancel"
|
|
342
395
|
>
|
|
343
396
|
<For each={picker()!.cals}>
|
|
@@ -387,33 +440,19 @@ function EventEditModal(props: { store: TuiStore }) {
|
|
|
387
440
|
props.store.closeModal();
|
|
388
441
|
return;
|
|
389
442
|
}
|
|
390
|
-
const
|
|
391
|
-
let title = trimmed;
|
|
392
|
-
let startMin = s.startMin;
|
|
393
|
-
let endMin = s.endMin;
|
|
394
|
-
// Peel a trailing time token: "Standup 9:00-9:30" / "Lunch 12-13".
|
|
395
|
-
const m = trimmed.match(/\s(\S+)$/);
|
|
396
|
-
const tok = m?.[1];
|
|
397
|
-
if (m && m.index !== undefined && tok) {
|
|
398
|
-
const tb = parseTimeBlockShortcut(tok);
|
|
399
|
-
if (tb && tb.endMin > tb.startMin) {
|
|
400
|
-
title = trimmed.slice(0, m.index).trim();
|
|
401
|
-
startMin = tb.startMin;
|
|
402
|
-
endMin = tb.endMin;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
443
|
+
const { title, startMin, endMin, dateIso } = peelEventInput(text, s.startMin, s.endMin);
|
|
405
444
|
if (!title) {
|
|
406
445
|
setError("Title required");
|
|
407
446
|
return;
|
|
408
447
|
}
|
|
409
|
-
void props.store.confirmEventEdit(title, startMin, endMin);
|
|
448
|
+
void props.store.confirmEventEdit(title, startMin, endMin, dateIso);
|
|
410
449
|
}
|
|
411
450
|
|
|
412
451
|
return (
|
|
413
452
|
<Show when={sel()}>
|
|
414
453
|
<DialogShell
|
|
415
|
-
title=
|
|
416
|
-
hint="
|
|
454
|
+
title={`Edit event · ${shortDate(sel()!.dateIso)}`}
|
|
455
|
+
hint="change title, HH:MM-HH:MM, and/or a date (tm · +3 · lun · 2026-06-10) · Enter save · Esc"
|
|
417
456
|
>
|
|
418
457
|
<input
|
|
419
458
|
focused
|
|
@@ -789,6 +828,7 @@ function HelpModal(props: { store: TuiStore }) {
|
|
|
789
828
|
<span style={{ fg: T.text }}>{" \\ Jump back to today\n"}</span>
|
|
790
829
|
<span style={{ fg: T.textDim }}>{"\nAgenda (timeline) scheduling\n"}</span>
|
|
791
830
|
<span style={{ fg: T.text }}>{" n / click slot New Google Calendar event (needs: calendar-setup google --write)\n"}</span>
|
|
831
|
+
<span style={{ fg: T.text }}>{" append date+time: Lunch tomorrow 12-13 · Review 2026-06-10 15-16 · Holiday 25-12 allday\n"}</span>
|
|
792
832
|
<span style={{ fg: T.text }}>{" click an event Select an editable Google event — then e edit · d delete · Esc\n"}</span>
|
|
793
833
|
<span style={{ fg: T.text }}>{" c (any zone) Toggle ARM MODE — then click a task, click a slot, repeat\n"}</span>
|
|
794
834
|
<span style={{ fg: T.text }}>{" click empty row Place the armed task here (30-min block, or move if it has one)\n"}</span>
|
package/src/ui/TimelineView.tsx
CHANGED
|
@@ -115,9 +115,13 @@ export function TimelineView(props: TimelineViewProps) {
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
// Read-only calendar events (Google / Microsoft), merged into the grid for
|
|
118
|
-
// display only — not cursor-navigable
|
|
118
|
+
// display only — not cursor-navigable. Timed events go on the 24h grid;
|
|
119
|
+
// all-day events are pulled out into a chip strip at the top (no time slot).
|
|
119
120
|
const calEntries = createMemo(() =>
|
|
120
|
-
buildCalendarEntries(props.store.calendar.events()),
|
|
121
|
+
buildCalendarEntries(props.store.calendar.events().filter((e) => !e.allDay)),
|
|
122
|
+
);
|
|
123
|
+
const allDayEvents = createMemo(() =>
|
|
124
|
+
props.store.calendar.events().filter((e) => e.allDay),
|
|
121
125
|
);
|
|
122
126
|
|
|
123
127
|
// Recompute the row map every minute so the "now" marker stays current.
|
|
@@ -405,9 +409,32 @@ export function TimelineView(props: TimelineViewProps) {
|
|
|
405
409
|
</text>
|
|
406
410
|
</Show>
|
|
407
411
|
|
|
408
|
-
{/*
|
|
409
|
-
|
|
410
|
-
|
|
412
|
+
{/* All-day events ride in a chip strip above the 24h grid (like Google
|
|
413
|
+
Calendar's top band) — they have no time slot to sit in. Display only. */}
|
|
414
|
+
<Show when={allDayEvents().length > 0}>
|
|
415
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
416
|
+
<text wrapMode="none" style={{ flexShrink: 0 }}>
|
|
417
|
+
<span style={{ fg: T.textDim }}>{"▦ "}</span>
|
|
418
|
+
</text>
|
|
419
|
+
<For each={allDayEvents().slice(0, 8)}>
|
|
420
|
+
{(e) => (
|
|
421
|
+
<text wrapMode="none" truncate style={{ flexShrink: 1, marginRight: 1 }}>
|
|
422
|
+
<span style={{ fg: e.color }}>{"●"}</span>
|
|
423
|
+
<span style={{ fg: T.text }}>{" " + tailTruncate(e.title, 18)}</span>
|
|
424
|
+
</text>
|
|
425
|
+
)}
|
|
426
|
+
</For>
|
|
427
|
+
<Show when={allDayEvents().length > 8}>
|
|
428
|
+
<text wrapMode="none" style={{ flexShrink: 0 }}>
|
|
429
|
+
<span style={{ fg: T.textDim }}>{`+${allDayEvents().length - 8}`}</span>
|
|
430
|
+
</text>
|
|
431
|
+
</Show>
|
|
432
|
+
</box>
|
|
433
|
+
</Show>
|
|
434
|
+
|
|
435
|
+
{/* The 24h grid owns the rest of the panel. Tasks are armed for scheduling
|
|
436
|
+
from the board / planner panel via the `C` shortcut, then placed by
|
|
437
|
+
clicking a slot here. */}
|
|
411
438
|
<scrollbox
|
|
412
439
|
ref={(r: ScrollBoxLike) => (scrollBoxRef = r)}
|
|
413
440
|
style={{
|