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.
@@ -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. All-day
69
- # events are skipped; each calendar keeps its own color. Paths support `~`.
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 read-only events from Google
183
- Calendar and Microsoft 365 alongside your time-blocked tasks — rendered as
184
- colored `📅` blocks you can't edit, so the day's real shape is visible at a
185
- glance. All-day events are skipped; each calendar keeps its own color. Events
186
- are cached 30 min on disk and refreshed every 5 min. **Bring your own
187
- credentials** — there's nothing to sign up for and nothing leaves your machine.
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 (append `HH:MM-HH:MM` to change the
257
- time), press Enter, pick the target calendar with `j`/`k`, Enter to create. Only
258
- calendars you can write to (owner/writer) show in the picker.
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 either (`Title HH:MM-HH:MM`) and Enter to save.
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.0",
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",
@@ -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) continue; // skip all-day (date only)
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) continue; // mirror Google: skip all-day events
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;
@@ -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
- async function confirmEventEdit(title: string, startMin: number, endMin: number): Promise<void> {
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 trimmed = text.trim();
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)} · append HH:MM-HH:MM to change · Enter add · Esc cancel`}
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 trimmed = text.trim();
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="Edit event"
416
- hint="append HH:MM-HH:MM to change the time · Enter save · Esc cancel"
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>
@@ -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, not editable.
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
- {/* The 24h grid now owns the whole panel no sticky section above
409
- it. Tasks are armed for scheduling from the board / planner panel
410
- via the `C` shortcut, then placed by clicking a slot here. */}
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={{