floq 1.3.0 → 1.3.2
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/calendar/index.d.ts +5 -0
- package/dist/calendar/index.js +24 -18
- package/dist/i18n/en.d.ts +26 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/ja.js +14 -0
- package/dist/ui/components/CalendarEvents.js +5 -1
- package/dist/ui/components/CalendarModal.js +221 -19
- package/dist/ui/components/GtdDQ.js +1 -1
- package/dist/ui/components/GtdMario.js +1 -1
- package/dist/ui/components/KanbanBoard.js +1 -1
- package/dist/ui/components/KanbanDQ.js +1 -1
- package/dist/ui/components/KanbanMario.js +1 -1
- package/package.json +1 -1
package/dist/calendar/index.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export declare function fetchCalendarEvents(url?: string): Promise<CalendarEvent
|
|
|
14
14
|
* Get cached events or fetch if cache is stale
|
|
15
15
|
*/
|
|
16
16
|
export declare function getCalendarEvents(): Promise<CalendarEvent[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Get events for a specific date from cache (synchronous)
|
|
19
|
+
* @param dayOffset - Number of days from today (0 = today, -1 = yesterday, 1 = tomorrow)
|
|
20
|
+
*/
|
|
21
|
+
export declare function getEventsForDate(dayOffset?: number): CalendarEvent[];
|
|
17
22
|
/**
|
|
18
23
|
* Get today's events from cache (synchronous)
|
|
19
24
|
*/
|
package/dist/calendar/index.js
CHANGED
|
@@ -15,11 +15,10 @@ function parseICalData(icalData) {
|
|
|
15
15
|
const vevents = vcalendar.getAllSubcomponents('vevent');
|
|
16
16
|
for (const vevent of vevents) {
|
|
17
17
|
const event = new ICAL.Event(vevent);
|
|
18
|
-
// Handle recurring events - get occurrences for
|
|
18
|
+
// Handle recurring events - get occurrences for current month and next month
|
|
19
19
|
const now = new Date();
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
|
20
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
21
|
+
const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
|
|
23
22
|
if (event.isRecurring()) {
|
|
24
23
|
try {
|
|
25
24
|
const iter = event.iterator();
|
|
@@ -30,7 +29,7 @@ function parseICalData(icalData) {
|
|
|
30
29
|
const occurrenceStart = next.toJSDate();
|
|
31
30
|
const occurrenceEnd = new Date(occurrenceStart.getTime() + event.duration.toSeconds() * 1000);
|
|
32
31
|
// Only include occurrences within our time window
|
|
33
|
-
if (occurrenceStart >=
|
|
32
|
+
if (occurrenceStart >= startOfMonth && occurrenceStart <= endOfNextMonth) {
|
|
34
33
|
events.push({
|
|
35
34
|
id: `${event.uid}-${occurrenceStart.getTime()}`,
|
|
36
35
|
title: event.summary || 'Untitled',
|
|
@@ -41,7 +40,7 @@ function parseICalData(icalData) {
|
|
|
41
40
|
});
|
|
42
41
|
}
|
|
43
42
|
// Stop if we're past our window
|
|
44
|
-
if (occurrenceStart
|
|
43
|
+
if (occurrenceStart > endOfNextMonth)
|
|
45
44
|
break;
|
|
46
45
|
next = iter.next();
|
|
47
46
|
count++;
|
|
@@ -104,11 +103,10 @@ async function fetchEventsViaOAuth() {
|
|
|
104
103
|
return [];
|
|
105
104
|
}
|
|
106
105
|
const now = new Date();
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
|
106
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
107
|
+
const endOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0); // Last day of next month
|
|
110
108
|
try {
|
|
111
|
-
return await listGoogleEvents(accessToken, oauthConfig.calendarId,
|
|
109
|
+
return await listGoogleEvents(accessToken, oauthConfig.calendarId, startOfMonth, endOfNextMonth);
|
|
112
110
|
}
|
|
113
111
|
catch (error) {
|
|
114
112
|
console.error('Failed to fetch Google Calendar events:', error);
|
|
@@ -182,21 +180,23 @@ export async function getCalendarEvents() {
|
|
|
182
180
|
return fetchCalendarEvents();
|
|
183
181
|
}
|
|
184
182
|
/**
|
|
185
|
-
* Get
|
|
183
|
+
* Get events for a specific date from cache (synchronous)
|
|
184
|
+
* @param dayOffset - Number of days from today (0 = today, -1 = yesterday, 1 = tomorrow)
|
|
186
185
|
*/
|
|
187
|
-
export function
|
|
186
|
+
export function getEventsForDate(dayOffset = 0) {
|
|
188
187
|
if (!eventsCache) {
|
|
189
188
|
return [];
|
|
190
189
|
}
|
|
191
190
|
const now = new Date();
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
const targetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
192
|
+
targetDate.setDate(targetDate.getDate() + dayOffset);
|
|
193
|
+
const endOfTarget = new Date(targetDate);
|
|
194
|
+
endOfTarget.setDate(endOfTarget.getDate() + 1);
|
|
195
195
|
return eventsCache.events
|
|
196
196
|
.filter(event => {
|
|
197
|
-
// Event starts
|
|
198
|
-
return (event.start >=
|
|
199
|
-
(event.start <
|
|
197
|
+
// Event starts on target date or spans target date
|
|
198
|
+
return (event.start >= targetDate && event.start < endOfTarget) ||
|
|
199
|
+
(event.start < targetDate && event.end >= targetDate);
|
|
200
200
|
})
|
|
201
201
|
.sort((a, b) => {
|
|
202
202
|
// All-day events first, then by start time
|
|
@@ -207,6 +207,12 @@ export function getTodayEvents() {
|
|
|
207
207
|
return a.start.getTime() - b.start.getTime();
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Get today's events from cache (synchronous)
|
|
212
|
+
*/
|
|
213
|
+
export function getTodayEvents() {
|
|
214
|
+
return getEventsForDate(0);
|
|
215
|
+
}
|
|
210
216
|
/**
|
|
211
217
|
* Get upcoming events (next event for each hour slot)
|
|
212
218
|
*/
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -260,12 +260,25 @@ export declare const en: {
|
|
|
260
260
|
calendar: {
|
|
261
261
|
label: string;
|
|
262
262
|
noEvents: string;
|
|
263
|
+
noUpcoming: string;
|
|
263
264
|
allDay: string;
|
|
264
265
|
upcoming: string;
|
|
265
266
|
more: string;
|
|
266
267
|
modalTitle: string;
|
|
268
|
+
yesterday: string;
|
|
269
|
+
today: string;
|
|
270
|
+
tomorrow: string;
|
|
271
|
+
noEventsForDay: string;
|
|
267
272
|
notConfigured: string;
|
|
268
273
|
setupHint: string;
|
|
274
|
+
addEvent: string;
|
|
275
|
+
eventTitle: string;
|
|
276
|
+
eventStart: string;
|
|
277
|
+
eventEnd: string;
|
|
278
|
+
eventAllDay: string;
|
|
279
|
+
eventCreated: string;
|
|
280
|
+
eventCreateError: string;
|
|
281
|
+
addNotSupported: string;
|
|
269
282
|
oauthConfigured: string;
|
|
270
283
|
oauthNotConfigured: string;
|
|
271
284
|
loginRequired: string;
|
|
@@ -480,12 +493,25 @@ export type PomodoroTranslations = {
|
|
|
480
493
|
export type CalendarTranslations = {
|
|
481
494
|
label: string;
|
|
482
495
|
noEvents: string;
|
|
496
|
+
noUpcoming?: string;
|
|
483
497
|
allDay: string;
|
|
484
498
|
upcoming: string;
|
|
485
499
|
more: string;
|
|
486
500
|
modalTitle?: string;
|
|
501
|
+
yesterday?: string;
|
|
502
|
+
today?: string;
|
|
503
|
+
tomorrow?: string;
|
|
504
|
+
noEventsForDay?: string;
|
|
487
505
|
notConfigured?: string;
|
|
488
506
|
setupHint?: string;
|
|
507
|
+
addEvent?: string;
|
|
508
|
+
eventTitle?: string;
|
|
509
|
+
eventStart?: string;
|
|
510
|
+
eventEnd?: string;
|
|
511
|
+
eventAllDay?: string;
|
|
512
|
+
eventCreated?: string;
|
|
513
|
+
eventCreateError?: string;
|
|
514
|
+
addNotSupported?: string;
|
|
489
515
|
oauthConfigured?: string;
|
|
490
516
|
oauthNotConfigured?: string;
|
|
491
517
|
loginRequired?: string;
|
package/dist/i18n/en.js
CHANGED
|
@@ -278,13 +278,27 @@ export const en = {
|
|
|
278
278
|
calendar: {
|
|
279
279
|
label: '[CAL]',
|
|
280
280
|
noEvents: 'No events today',
|
|
281
|
+
noUpcoming: 'No more events. Good work today!',
|
|
281
282
|
allDay: 'All day',
|
|
282
283
|
upcoming: 'Next:',
|
|
283
284
|
more: '+{count}',
|
|
284
285
|
// Modal
|
|
285
286
|
modalTitle: "Today's Events",
|
|
287
|
+
yesterday: 'Yesterday',
|
|
288
|
+
today: 'Today',
|
|
289
|
+
tomorrow: 'Tomorrow',
|
|
290
|
+
noEventsForDay: 'No events',
|
|
286
291
|
notConfigured: 'Calendar not configured.',
|
|
287
292
|
setupHint: 'Run "floq calendar --help" to set up.',
|
|
293
|
+
// Add event
|
|
294
|
+
addEvent: 'Add Event',
|
|
295
|
+
eventTitle: 'Title',
|
|
296
|
+
eventStart: 'Start',
|
|
297
|
+
eventEnd: 'End',
|
|
298
|
+
eventAllDay: 'All day',
|
|
299
|
+
eventCreated: 'Event created',
|
|
300
|
+
eventCreateError: 'Failed to create event',
|
|
301
|
+
addNotSupported: 'Event creation requires OAuth login',
|
|
288
302
|
// OAuth messages
|
|
289
303
|
oauthConfigured: 'OAuth client configured',
|
|
290
304
|
oauthNotConfigured: 'OAuth client not configured',
|
package/dist/i18n/ja.js
CHANGED
|
@@ -278,13 +278,27 @@ export const ja = {
|
|
|
278
278
|
calendar: {
|
|
279
279
|
label: '予定',
|
|
280
280
|
noEvents: '今日の予定はありません',
|
|
281
|
+
noUpcoming: '次の予定はありません。おつかれさまでした!',
|
|
281
282
|
allDay: '終日',
|
|
282
283
|
upcoming: '次:',
|
|
283
284
|
more: '+{count}',
|
|
284
285
|
// Modal
|
|
285
286
|
modalTitle: '今日の予定',
|
|
287
|
+
yesterday: '昨日',
|
|
288
|
+
today: '今日',
|
|
289
|
+
tomorrow: '明日',
|
|
290
|
+
noEventsForDay: '予定なし',
|
|
286
291
|
notConfigured: 'カレンダーが設定されていません。',
|
|
287
292
|
setupHint: '"floq calendar --help" で設定方法を確認してください。',
|
|
293
|
+
// Add event
|
|
294
|
+
addEvent: '予定を追加',
|
|
295
|
+
eventTitle: 'タイトル',
|
|
296
|
+
eventStart: '開始',
|
|
297
|
+
eventEnd: '終了',
|
|
298
|
+
eventAllDay: '終日',
|
|
299
|
+
eventCreated: '予定を作成しました',
|
|
300
|
+
eventCreateError: '予定の作成に失敗しました',
|
|
301
|
+
addNotSupported: '予定の追加にはOAuthログインが必要です',
|
|
288
302
|
// OAuth messages
|
|
289
303
|
oauthConfigured: 'OAuthクライアント設定済み',
|
|
290
304
|
oauthNotConfigured: 'OAuthクライアント未設定',
|
|
@@ -62,8 +62,12 @@ export function CalendarEvents({ maxEvents = 3, showLabel = true, compact = true
|
|
|
62
62
|
const now = new Date();
|
|
63
63
|
const upcomingEvents = events.filter(e => !e.allDay && e.end > now);
|
|
64
64
|
const nextEvent = upcomingEvents[0];
|
|
65
|
-
if (compact
|
|
65
|
+
if (compact) {
|
|
66
66
|
// Compact mode: show only the next event with start-end time
|
|
67
|
+
// If no upcoming events, show a friendly message
|
|
68
|
+
if (!nextEvent) {
|
|
69
|
+
return (_jsxs(Box, { children: [showLabel && (_jsxs(Text, { color: theme.colors.secondary, children: [i18n.tui.calendar?.label || '[CAL]', ' '] })), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.calendar?.noUpcoming || 'No more events. Good work today!' }), withSeparator && _jsx(Text, { color: theme.colors.textMuted, children: " | " })] }));
|
|
70
|
+
}
|
|
67
71
|
const isOngoing = isEventOngoing(nextEvent);
|
|
68
72
|
const timeDisplay = `${formatEventTime(nextEvent.start)}-${formatEventTime(nextEvent.end)}`;
|
|
69
73
|
// Truncate title if too long
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { t } from '../../i18n/index.js';
|
|
5
5
|
import { useTheme } from '../theme/index.js';
|
|
6
6
|
import { isCalendarEnabled, getCalendarConfig, getCalendarType } from '../../config.js';
|
|
7
|
-
import { getCalendarEvents,
|
|
8
|
-
const
|
|
7
|
+
import { getCalendarEvents, getEventsForDate, formatEventTime, isEventOngoing, } from '../../calendar/index.js';
|
|
8
|
+
const WEEKDAYS_EN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
9
|
+
const WEEKDAYS_JA = ['日', '月', '火', '水', '木', '金', '土'];
|
|
10
|
+
const VISIBLE_EVENTS = 5;
|
|
9
11
|
export function CalendarModal({ onClose }) {
|
|
10
12
|
const theme = useTheme();
|
|
11
13
|
const i18n = t();
|
|
12
14
|
const [events, setEvents] = useState([]);
|
|
15
|
+
const [allEvents, setAllEvents] = useState([]);
|
|
13
16
|
const [isLoading, setIsLoading] = useState(true);
|
|
14
17
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
18
|
+
const [selectedDate, setSelectedDate] = useState(new Date());
|
|
19
|
+
const [viewMonth, setViewMonth] = useState(new Date());
|
|
20
|
+
const [mode, setMode] = useState('calendar');
|
|
15
21
|
const calendarEnabled = isCalendarEnabled();
|
|
16
22
|
const config = getCalendarConfig();
|
|
17
23
|
const calendarType = getCalendarType();
|
|
24
|
+
const isJapanese = i18n.tui.calendar?.yesterday === '昨日';
|
|
25
|
+
const weekdays = isJapanese ? WEEKDAYS_JA : WEEKDAYS_EN;
|
|
26
|
+
// Load all events on mount
|
|
18
27
|
useEffect(() => {
|
|
19
28
|
if (!calendarEnabled) {
|
|
20
29
|
setIsLoading(false);
|
|
@@ -23,9 +32,9 @@ export function CalendarModal({ onClose }) {
|
|
|
23
32
|
let mounted = true;
|
|
24
33
|
const loadEvents = async () => {
|
|
25
34
|
try {
|
|
26
|
-
await getCalendarEvents();
|
|
35
|
+
const loadedEvents = await getCalendarEvents();
|
|
27
36
|
if (mounted) {
|
|
28
|
-
|
|
37
|
+
setAllEvents(loadedEvents);
|
|
29
38
|
setIsLoading(false);
|
|
30
39
|
}
|
|
31
40
|
}
|
|
@@ -40,17 +49,163 @@ export function CalendarModal({ onClose }) {
|
|
|
40
49
|
mounted = false;
|
|
41
50
|
};
|
|
42
51
|
}, [calendarEnabled]);
|
|
43
|
-
|
|
52
|
+
// Update events when selected date changes
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!isLoading) {
|
|
55
|
+
const today = new Date();
|
|
56
|
+
today.setHours(0, 0, 0, 0);
|
|
57
|
+
const selected = new Date(selectedDate);
|
|
58
|
+
selected.setHours(0, 0, 0, 0);
|
|
59
|
+
const dayOffset = Math.round((selected.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
60
|
+
setEvents(getEventsForDate(dayOffset));
|
|
61
|
+
setScrollOffset(0);
|
|
62
|
+
}
|
|
63
|
+
}, [selectedDate, isLoading, allEvents]);
|
|
64
|
+
// Generate calendar grid for the view month
|
|
65
|
+
const calendarGrid = useMemo(() => {
|
|
66
|
+
const year = viewMonth.getFullYear();
|
|
67
|
+
const month = viewMonth.getMonth();
|
|
68
|
+
const firstDay = new Date(year, month, 1);
|
|
69
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
70
|
+
const startDayOfWeek = firstDay.getDay();
|
|
71
|
+
const daysInMonth = lastDay.getDate();
|
|
72
|
+
const grid = [];
|
|
73
|
+
let currentDay = 1;
|
|
74
|
+
for (let week = 0; week < 6; week++) {
|
|
75
|
+
const row = [];
|
|
76
|
+
for (let day = 0; day < 7; day++) {
|
|
77
|
+
if (week === 0 && day < startDayOfWeek) {
|
|
78
|
+
row.push(null);
|
|
79
|
+
}
|
|
80
|
+
else if (currentDay > daysInMonth) {
|
|
81
|
+
row.push(null);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
row.push(currentDay);
|
|
85
|
+
currentDay++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
grid.push(row);
|
|
89
|
+
if (currentDay > daysInMonth)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return grid;
|
|
93
|
+
}, [viewMonth]);
|
|
94
|
+
// Check if a date has events
|
|
95
|
+
const hasEventsOnDay = (day) => {
|
|
96
|
+
const date = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), day);
|
|
97
|
+
const nextDate = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), day + 1);
|
|
98
|
+
return allEvents.some(event => (event.start >= date && event.start < nextDate) ||
|
|
99
|
+
(event.start < date && event.end >= date));
|
|
100
|
+
};
|
|
101
|
+
// Check if a date is today
|
|
102
|
+
const isToday = (day) => {
|
|
103
|
+
const today = new Date();
|
|
104
|
+
return day === today.getDate() &&
|
|
105
|
+
viewMonth.getMonth() === today.getMonth() &&
|
|
106
|
+
viewMonth.getFullYear() === today.getFullYear();
|
|
107
|
+
};
|
|
108
|
+
// Check if a date is selected
|
|
109
|
+
const isSelected = (day) => {
|
|
110
|
+
return day === selectedDate.getDate() &&
|
|
111
|
+
viewMonth.getMonth() === selectedDate.getMonth() &&
|
|
112
|
+
viewMonth.getFullYear() === selectedDate.getFullYear();
|
|
113
|
+
};
|
|
114
|
+
const maxScroll = Math.max(0, events.length - VISIBLE_EVENTS);
|
|
44
115
|
useInput((input, key) => {
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
// Events mode: scroll through events
|
|
117
|
+
if (mode === 'events') {
|
|
118
|
+
if (input === 'j' || key.downArrow) {
|
|
119
|
+
setScrollOffset(prev => Math.min(prev + 1, maxScroll));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input === 'k' || key.upArrow) {
|
|
123
|
+
setScrollOffset(prev => Math.max(prev - 1, 0));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (key.escape || input === 'q') {
|
|
127
|
+
setMode('calendar');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Calendar mode: navigate dates
|
|
133
|
+
if (input === 'h' || key.leftArrow) {
|
|
134
|
+
setSelectedDate(prev => {
|
|
135
|
+
const newDate = new Date(prev);
|
|
136
|
+
newDate.setDate(newDate.getDate() - 1);
|
|
137
|
+
if (newDate.getMonth() !== viewMonth.getMonth()) {
|
|
138
|
+
setViewMonth(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
|
|
139
|
+
}
|
|
140
|
+
return newDate;
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (input === 'l' || key.rightArrow) {
|
|
145
|
+
setSelectedDate(prev => {
|
|
146
|
+
const newDate = new Date(prev);
|
|
147
|
+
newDate.setDate(newDate.getDate() + 1);
|
|
148
|
+
if (newDate.getMonth() !== viewMonth.getMonth()) {
|
|
149
|
+
setViewMonth(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
|
|
150
|
+
}
|
|
151
|
+
return newDate;
|
|
152
|
+
});
|
|
47
153
|
return;
|
|
48
154
|
}
|
|
49
155
|
if (input === 'k' || key.upArrow) {
|
|
50
|
-
|
|
156
|
+
setSelectedDate(prev => {
|
|
157
|
+
const newDate = new Date(prev);
|
|
158
|
+
newDate.setDate(newDate.getDate() - 7);
|
|
159
|
+
if (newDate.getMonth() !== viewMonth.getMonth()) {
|
|
160
|
+
setViewMonth(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
|
|
161
|
+
}
|
|
162
|
+
return newDate;
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (input === 'j' || key.downArrow) {
|
|
167
|
+
setSelectedDate(prev => {
|
|
168
|
+
const newDate = new Date(prev);
|
|
169
|
+
newDate.setDate(newDate.getDate() + 7);
|
|
170
|
+
if (newDate.getMonth() !== viewMonth.getMonth()) {
|
|
171
|
+
setViewMonth(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
|
|
172
|
+
}
|
|
173
|
+
return newDate;
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Enter events mode
|
|
178
|
+
if (key.return && events.length > 0) {
|
|
179
|
+
setMode('events');
|
|
51
180
|
return;
|
|
52
181
|
}
|
|
53
|
-
|
|
182
|
+
// Previous/Next month
|
|
183
|
+
if (input === 'H') {
|
|
184
|
+
setViewMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1, 1));
|
|
185
|
+
setSelectedDate(prev => {
|
|
186
|
+
const newDate = new Date(prev);
|
|
187
|
+
newDate.setMonth(newDate.getMonth() - 1);
|
|
188
|
+
return newDate;
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (input === 'L') {
|
|
193
|
+
setViewMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1, 1));
|
|
194
|
+
setSelectedDate(prev => {
|
|
195
|
+
const newDate = new Date(prev);
|
|
196
|
+
newDate.setMonth(newDate.getMonth() + 1);
|
|
197
|
+
return newDate;
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Go to today
|
|
202
|
+
if (input === 't') {
|
|
203
|
+
const today = new Date();
|
|
204
|
+
setSelectedDate(today);
|
|
205
|
+
setViewMonth(new Date(today.getFullYear(), today.getMonth(), 1));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (key.escape || input === 'q' || input === ' ') {
|
|
54
209
|
onClose();
|
|
55
210
|
return;
|
|
56
211
|
}
|
|
@@ -60,14 +215,61 @@ export function CalendarModal({ onClose }) {
|
|
|
60
215
|
const calendarName = calendarType === 'oauth' && config?.oauth
|
|
61
216
|
? config.oauth.calendarName
|
|
62
217
|
: config?.name || 'Calendar';
|
|
63
|
-
|
|
218
|
+
// Format month header
|
|
219
|
+
const monthNames = isJapanese
|
|
220
|
+
? ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
|
221
|
+
: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
222
|
+
const monthHeader = isJapanese
|
|
223
|
+
? `${viewMonth.getFullYear()}年 ${monthNames[viewMonth.getMonth()]}`
|
|
224
|
+
: `${monthNames[viewMonth.getMonth()]} ${viewMonth.getFullYear()}`;
|
|
225
|
+
// Format selected date
|
|
226
|
+
const formatSelectedDate = () => {
|
|
227
|
+
const today = new Date();
|
|
228
|
+
today.setHours(0, 0, 0, 0);
|
|
229
|
+
const selected = new Date(selectedDate);
|
|
230
|
+
selected.setHours(0, 0, 0, 0);
|
|
231
|
+
const diff = Math.round((selected.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
232
|
+
if (diff === -1)
|
|
233
|
+
return i18n.tui.calendar?.yesterday || 'Yesterday';
|
|
234
|
+
if (diff === 0)
|
|
235
|
+
return i18n.tui.calendar?.today || 'Today';
|
|
236
|
+
if (diff === 1)
|
|
237
|
+
return i18n.tui.calendar?.tomorrow || 'Tomorrow';
|
|
238
|
+
if (isJapanese) {
|
|
239
|
+
return `${selectedDate.getMonth() + 1}/${selectedDate.getDate()} (${weekdays[selectedDate.getDay()]})`;
|
|
240
|
+
}
|
|
241
|
+
return `${monthNames[selectedDate.getMonth()].slice(0, 3)} ${selectedDate.getDate()}`;
|
|
242
|
+
};
|
|
243
|
+
const visibleEvents = events.slice(scrollOffset, scrollOffset + VISIBLE_EVENTS);
|
|
64
244
|
const showScrollUp = scrollOffset > 0;
|
|
65
245
|
const showScrollDown = scrollOffset < maxScroll;
|
|
66
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
246
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.secondary, children: formatTitle(calendarName) }) }), !calendarEnabled ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.calendar?.notConfigured || 'Calendar not configured.' }), _jsx(Text, { color: theme.colors.textMuted, children: " " }), _jsx(Text, { color: theme.colors.text, children: i18n.tui.calendar?.setupHint || 'Run "floq calendar --help" to set up.' })] })) : isLoading ? (_jsx(Text, { color: theme.colors.textMuted, children: "Loading..." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, children: '◀ ' }), _jsx(Text, { bold: true, color: theme.colors.text, children: monthHeader }), _jsx(Text, { color: theme.colors.text, children: ' ▶' })] }), _jsx(Box, { children: weekdays.map((day, i) => (_jsx(Box, { width: 4, justifyContent: "center", children: _jsx(Text, { color: i === 0 ? theme.colors.accent : (i === 6 ? theme.colors.secondary : theme.colors.textMuted), children: day }) }, day))) }), calendarGrid.map((week, weekIndex) => (_jsx(Box, { children: week.map((day, dayIndex) => {
|
|
247
|
+
if (day === null) {
|
|
248
|
+
return _jsx(Box, { width: 4, children: _jsx(Text, { children: " " }) }, dayIndex);
|
|
249
|
+
}
|
|
250
|
+
const selected = isSelected(day);
|
|
251
|
+
const today = isToday(day);
|
|
252
|
+
const hasEvents = hasEventsOnDay(day);
|
|
253
|
+
const isSunday = dayIndex === 0;
|
|
254
|
+
const isSaturday = dayIndex === 6;
|
|
255
|
+
let color = theme.colors.text;
|
|
256
|
+
if (isSunday)
|
|
257
|
+
color = theme.colors.accent;
|
|
258
|
+
else if (isSaturday)
|
|
259
|
+
color = theme.colors.secondary;
|
|
260
|
+
const dayStr = day.toString().padStart(2, ' ');
|
|
261
|
+
return (_jsx(Box, { width: 4, justifyContent: "center", children: selected ? (_jsx(Text, { backgroundColor: theme.colors.accent, color: theme.colors.background, children: hasEvents ? `${dayStr}*` : `${dayStr} ` })) : today ? (_jsx(Text, { bold: true, underline: true, color: color, children: hasEvents ? `${dayStr}*` : `${dayStr} ` })) : (_jsx(Text, { color: color, children: hasEvents ? `${dayStr}*` : `${dayStr} ` })) }, dayIndex));
|
|
262
|
+
}) }, weekIndex)))] }), _jsx(Box, { marginY: 1, justifyContent: "center", children: _jsx(Text, { color: theme.colors.border, children: '─'.repeat(28) }) }), _jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: mode === 'events' ? theme.colors.accent : theme.colors.secondary, children: [formatSelectedDate(), mode === 'events' ? ' *' : ''] }) }), _jsxs(Box, { flexDirection: "column", width: 28, children: [showScrollUp && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25B2 (k)" })), events.length === 0 ? (_jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.calendar?.noEventsForDay || 'No events' }) })) : (visibleEvents.map((event, index) => {
|
|
263
|
+
const isOngoing = isEventOngoing(event);
|
|
264
|
+
const timeStr = event.allDay
|
|
265
|
+
? (i18n.tui.calendar?.allDay || 'All day')
|
|
266
|
+
: formatEventTime(event.start);
|
|
267
|
+
const maxTitleLen = 18;
|
|
268
|
+
const title = event.title.length > maxTitleLen
|
|
269
|
+
? event.title.slice(0, maxTitleLen - 1) + '…'
|
|
270
|
+
: event.title;
|
|
271
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isOngoing ? theme.colors.accent : theme.colors.secondary, children: timeStr.padEnd(8) }), _jsx(Text, { color: theme.colors.text, children: title })] }, event.id || index));
|
|
272
|
+
})), showScrollDown && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25BC (j)" })), events.length > VISIBLE_EVENTS && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { color: theme.colors.textMuted, children: ["+", events.length - VISIBLE_EVENTS, " more"] }) }))] })] })] })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: mode === 'events'
|
|
273
|
+
? 'j/k: scroll | q/Esc: back'
|
|
274
|
+
: `hjkl: move | H/L: month | t: today${events.length > 0 ? ' | Enter: events' : ''} | q: close` }) })] }));
|
|
73
275
|
}
|
|
@@ -1023,7 +1023,7 @@ export function GtdDQ({ onOpenSettings }) {
|
|
|
1023
1023
|
setMessage(focusMode ? 'Focus mode off' : 'Focus mode on');
|
|
1024
1024
|
return;
|
|
1025
1025
|
}
|
|
1026
|
-
});
|
|
1026
|
+
}, { isActive: mode !== 'calendar' });
|
|
1027
1027
|
const tursoEnabled = isTursoEnabled();
|
|
1028
1028
|
// Get parent project for display
|
|
1029
1029
|
const getParentProject = (parentId) => {
|
|
@@ -714,7 +714,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
714
714
|
});
|
|
715
715
|
return;
|
|
716
716
|
}
|
|
717
|
-
});
|
|
717
|
+
}, { isActive: mode !== 'calendar' });
|
|
718
718
|
// Help modal overlay
|
|
719
719
|
if (mode === 'help') {
|
|
720
720
|
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal'), isKanban: true }) }));
|