react-os-shell 0.7.5 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-os-shell",
3
- "version": "0.7.5",
4
- "description": "Desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted glass theming, and bundled apps. Email + Calendar talk to your own IMAP/SMTP/CalDAV provider via a colocated Node bridge server.",
3
+ "version": "0.9.0",
4
+ "description": "Desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted glass theming, and bundled apps.",
5
5
  "license": "MIT",
6
6
  "author": "Victor Y. Mau",
7
7
  "homepage": "https://github.com/victorymau/react-os-shell#readme",
@@ -53,7 +53,6 @@
53
53
  "devDependencies": {
54
54
  "@types/react": "^18.2.0",
55
55
  "@types/react-dom": "^18.2.0",
56
- "concurrently": "^8.2.2",
57
56
  "dxf-viewer": "^1.0.47",
58
57
  "mammoth": "^1.8.0",
59
58
  "online-3d-viewer": "^0.18.0",
@@ -70,13 +69,7 @@
70
69
  "dev": "tsup --watch",
71
70
  "typecheck": "tsc --noEmit",
72
71
  "screenshot": "node scripts/screenshot.mjs",
73
- "clean": "rm -rf dist",
74
- "server:install": "cd server && npm install",
75
- "server:dev": "cd server && npm run dev",
76
- "server:build": "cd server && npm run build",
77
- "server:start": "cd server && npm start",
78
- "server:typecheck": "cd server && npm run typecheck",
79
- "dev:all": "concurrently -n shell,server -c blue,green \"npm run dev\" \"npm run server:dev\""
72
+ "clean": "rm -rf dist"
80
73
  },
81
74
  "keywords": [
82
75
  "react",
@@ -1,598 +0,0 @@
1
- import { useMailAuth, getMailClient } from './chunk-VBFB3ZIN.js';
2
- import { useTodoTasks } from './chunk-FTL7F4TM.js';
3
- import { useShellPrefs } from './chunk-36VM54SC.js';
4
- import { toast_default } from './chunk-WIJ45SYD.js';
5
- import { Modal, ModalActions } from './chunk-3T6SQ4UO.js';
6
- import './chunk-UBN4IUDE.js';
7
- import './chunk-ZF6AYO4G.js';
8
- import { useState, useMemo, useEffect, useCallback } from 'react';
9
- import { jsxs, jsx } from 'react/jsx-runtime';
10
-
11
- var COLORS = [
12
- { key: "blue", bg: "bg-blue-500", light: "bg-blue-100 text-blue-800", dot: "bg-blue-500" },
13
- { key: "green", bg: "bg-green-500", light: "bg-green-100 text-green-800", dot: "bg-green-500" },
14
- { key: "red", bg: "bg-red-500", light: "bg-red-100 text-red-800", dot: "bg-red-500" },
15
- { key: "purple", bg: "bg-purple-500", light: "bg-purple-100 text-purple-800", dot: "bg-purple-500" },
16
- { key: "orange", bg: "bg-orange-500", light: "bg-orange-100 text-orange-800", dot: "bg-orange-500" },
17
- { key: "pink", bg: "bg-pink-500", light: "bg-pink-100 text-pink-800", dot: "bg-pink-500" },
18
- { key: "yellow", bg: "bg-yellow-500", light: "bg-yellow-100 text-yellow-800", dot: "bg-yellow-500" },
19
- { key: "gray", bg: "bg-gray-500", light: "bg-gray-100 text-gray-800", dot: "bg-gray-500" }
20
- ];
21
- function getColor(key) {
22
- return COLORS.find((c) => c.key === key) || COLORS[0];
23
- }
24
- var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
25
- function pad(n) {
26
- return String(n).padStart(2, "0");
27
- }
28
- function toDateStr(d) {
29
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
30
- }
31
- function getMonthDays(year, month) {
32
- const first = new Date(year, month, 1);
33
- const startDay = first.getDay();
34
- const daysInMonth = new Date(year, month + 1, 0).getDate();
35
- const prevDays = new Date(year, month, 0).getDate();
36
- const cells = [];
37
- for (let i = startDay - 1; i >= 0; i--) {
38
- cells.push({ date: new Date(year, month - 1, prevDays - i), isCurrentMonth: false });
39
- }
40
- for (let d = 1; d <= daysInMonth; d++) {
41
- cells.push({ date: new Date(year, month, d), isCurrentMonth: true });
42
- }
43
- const remaining = 42 - cells.length;
44
- for (let d = 1; d <= remaining; d++) {
45
- cells.push({ date: new Date(year, month + 1, d), isCurrentMonth: false });
46
- }
47
- return cells;
48
- }
49
- function getWeekDays(date) {
50
- const start = new Date(date);
51
- start.setDate(start.getDate() - start.getDay());
52
- const days = [];
53
- for (let i = 0; i < 7; i++) {
54
- const d = new Date(start);
55
- d.setDate(d.getDate() + i);
56
- days.push(d);
57
- }
58
- return days;
59
- }
60
- function davToLocal(ev, calendarId) {
61
- const startIso = ev.start;
62
- const endIso = ev.end;
63
- const startDate = startIso.slice(0, 10);
64
- const startTime = ev.allDay ? void 0 : startIso.slice(11, 16);
65
- const endTime = ev.allDay ? void 0 : endIso.slice(11, 16);
66
- return {
67
- id: `caldav-${calendarId}-${ev.uid}`,
68
- title: ev.summary || "(No title)",
69
- date: startDate,
70
- start_time: startTime,
71
- end_time: endTime,
72
- color: "blue",
73
- all_day: ev.allDay,
74
- description: ev.description || void 0,
75
- _caldav: { calendarId, eventUid: ev.uid, etag: ev.etag }
76
- };
77
- }
78
- function toDavInput(evt) {
79
- const start = evt.all_day ? evt.date : `${evt.date}T${evt.start_time || "09:00"}:00`;
80
- const end = evt.all_day ? evt.date : `${evt.date}T${evt.end_time || "10:00"}:00`;
81
- return {
82
- summary: evt.title,
83
- description: evt.description,
84
- start: new Date(start).toISOString(),
85
- end: new Date(end).toISOString(),
86
- allDay: !!evt.all_day
87
- };
88
- }
89
- function Calendar() {
90
- const { prefs, save } = useShellPrefs();
91
- const { isConnected, capabilities } = useMailAuth();
92
- const caldavEnabled = isConnected && capabilities?.caldav === true;
93
- const localEvents = prefs.calendar_events || [];
94
- const [calendars, setCalendars] = useState([]);
95
- const [caldavEvents, setCaldavEvents] = useState([]);
96
- const { tasks: todoTasks, toggleDone: toggleTodoDone } = useTodoTasks();
97
- const todoEvents = useMemo(
98
- () => todoTasks.filter((t) => !!t.dueDate).map((t) => ({
99
- id: `todo-${t.id}`,
100
- title: t.name,
101
- date: t.dueDate,
102
- color: "gray",
103
- _todo: true,
104
- _todoId: t.id,
105
- _done: t.done
106
- })),
107
- [todoTasks]
108
- );
109
- const events = useMemo(
110
- () => [...localEvents, ...caldavEvents, ...todoEvents],
111
- [localEvents, caldavEvents, todoEvents]
112
- );
113
- const today = /* @__PURE__ */ new Date();
114
- const [currentDate, setCurrentDate] = useState(new Date(today.getFullYear(), today.getMonth(), 1));
115
- const [view, setView] = useState("month");
116
- const [editingEvent, setEditingEvent] = useState(null);
117
- const [newEventDate, setNewEventDate] = useState(null);
118
- useEffect(() => {
119
- if (!caldavEnabled) {
120
- setCalendars([]);
121
- return;
122
- }
123
- getMailClient().get("/api/calendar/calendars").then((r) => setCalendars(r.data.calendars)).catch(() => setCalendars([]));
124
- }, [caldavEnabled]);
125
- useEffect(() => {
126
- if (!caldavEnabled || calendars.length === 0) {
127
- setCaldavEvents([]);
128
- return;
129
- }
130
- const y = currentDate.getFullYear();
131
- const m = currentDate.getMonth();
132
- const start = new Date(y, m - 1, 1).toISOString();
133
- const end = new Date(y, m + 2, 0).toISOString();
134
- let cancelled = false;
135
- Promise.all(
136
- calendars.map(
137
- (cal) => getMailClient().get(`/api/calendar/calendars/${encodeURIComponent(cal.id)}/events`, {
138
- params: { start, end }
139
- }).then((r) => r.data.events.map((e) => davToLocal(e, cal.id))).catch(() => [])
140
- )
141
- ).then((perCalendar) => {
142
- if (cancelled) return;
143
- setCaldavEvents(perCalendar.flat());
144
- });
145
- return () => {
146
- cancelled = true;
147
- };
148
- }, [caldavEnabled, calendars, currentDate]);
149
- const saveLocalEvents = useCallback((updated) => {
150
- save({ calendar_events: updated });
151
- }, [save]);
152
- const saveEvent = async (evt, targetCalendarId) => {
153
- if (evt._caldav) {
154
- try {
155
- const input = toDavInput(evt);
156
- const res = await getMailClient().put(
157
- `/api/calendar/calendars/${encodeURIComponent(evt._caldav.calendarId)}/events/${encodeURIComponent(evt._caldav.eventUid)}`,
158
- input,
159
- { headers: { "If-Match": evt._caldav.etag } }
160
- );
161
- setCaldavEvents((prev) => prev.map(
162
- (e) => e.id === evt.id ? { ...evt, _caldav: { ...evt._caldav, etag: res.data.etag } } : e
163
- ));
164
- toast_default.success("Event updated");
165
- } catch (err) {
166
- const status = err?.response?.status;
167
- if (status === 409) {
168
- toast_default.error("Event was modified elsewhere \u2014 refresh to see latest");
169
- } else {
170
- toast_default.error(extractError(err));
171
- }
172
- return;
173
- }
174
- } else if (targetCalendarId) {
175
- try {
176
- const input = toDavInput(evt);
177
- const res = await getMailClient().post(
178
- `/api/calendar/calendars/${encodeURIComponent(targetCalendarId)}/events`,
179
- input
180
- );
181
- const newEvt = {
182
- ...evt,
183
- id: `caldav-${targetCalendarId}-${res.data.uid}`,
184
- _caldav: { calendarId: targetCalendarId, eventUid: res.data.uid, etag: res.data.etag }
185
- };
186
- setCaldavEvents((prev) => [...prev, newEvt]);
187
- toast_default.success("Event created");
188
- } catch (err) {
189
- toast_default.error(extractError(err));
190
- return;
191
- }
192
- } else {
193
- const existing = localEvents.find((e) => e.id === evt.id);
194
- if (existing) {
195
- saveLocalEvents(localEvents.map((e) => e.id === evt.id ? evt : e));
196
- } else {
197
- saveLocalEvents([...localEvents, evt]);
198
- }
199
- }
200
- setEditingEvent(null);
201
- setNewEventDate(null);
202
- };
203
- const deleteEvent = async (evt) => {
204
- if (evt._caldav) {
205
- try {
206
- await getMailClient().delete(
207
- `/api/calendar/calendars/${encodeURIComponent(evt._caldav.calendarId)}/events/${encodeURIComponent(evt._caldav.eventUid)}`,
208
- { headers: { "If-Match": evt._caldav.etag } }
209
- );
210
- setCaldavEvents((prev) => prev.filter((e) => e.id !== evt.id));
211
- toast_default.success("Event deleted");
212
- } catch (err) {
213
- toast_default.error(extractError(err));
214
- return;
215
- }
216
- } else {
217
- saveLocalEvents(localEvents.filter((e) => e.id !== evt.id));
218
- }
219
- setEditingEvent(null);
220
- };
221
- const goToday = () => setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
222
- const goPrev = () => {
223
- if (view === "year") setCurrentDate(new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), 1));
224
- else if (view === "month") setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
225
- else {
226
- const d = new Date(currentDate);
227
- d.setDate(d.getDate() - 7);
228
- setCurrentDate(d);
229
- }
230
- };
231
- const goNext = () => {
232
- if (view === "year") setCurrentDate(new Date(currentDate.getFullYear() + 1, currentDate.getMonth(), 1));
233
- else if (view === "month") setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
234
- else {
235
- const d = new Date(currentDate);
236
- d.setDate(d.getDate() + 7);
237
- setCurrentDate(d);
238
- }
239
- };
240
- const monthLabel = view === "year" ? String(currentDate.getFullYear()) : currentDate.toLocaleDateString(void 0, { month: "long", year: "numeric" });
241
- const eventsByDate = useMemo(() => {
242
- const map = {};
243
- events.forEach((e) => {
244
- if (!map[e.date]) map[e.date] = [];
245
- map[e.date].push(e);
246
- });
247
- for (const key of Object.keys(map)) {
248
- map[key].sort((a, b) => (a.start_time || "").localeCompare(b.start_time || ""));
249
- }
250
- return map;
251
- }, [events]);
252
- const handleDayClick = (dateStr) => {
253
- setNewEventDate(dateStr);
254
- setEditingEvent({
255
- id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`,
256
- title: "",
257
- date: dateStr,
258
- start_time: "09:00",
259
- end_time: "10:00",
260
- color: "blue",
261
- all_day: false
262
- });
263
- };
264
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
265
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-gray-200 shrink-0", children: [
266
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
267
- /* @__PURE__ */ jsx("button", { onClick: goToday, className: "px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50", children: "Today" }),
268
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
269
- /* @__PURE__ */ jsx("button", { onClick: goPrev, className: "p-1 rounded hover:bg-gray-100", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" }) }) }),
270
- /* @__PURE__ */ jsx("button", { onClick: goNext, className: "p-1 rounded hover:bg-gray-100", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.25 4.5l7.5 7.5-7.5 7.5" }) }) })
271
- ] }),
272
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-gray-900", children: monthLabel })
273
- ] }),
274
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
275
- /* @__PURE__ */ jsx(
276
- "span",
277
- {
278
- onClick: () => window.dispatchEvent(new Event("open-mail-connect")),
279
- className: "text-[11px] text-gray-600 cursor-pointer hover:text-gray-900",
280
- title: "Open mail & calendar settings",
281
- children: caldavEnabled ? `CalDAV \xB7 ${calendars.length} calendar${calendars.length === 1 ? "" : "s"}` : "CalDAV not connected"
282
- }
283
- ),
284
- /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-gray-200" }),
285
- /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: ["year", "month", "week"].map((v) => /* @__PURE__ */ jsx(
286
- "button",
287
- {
288
- onClick: () => setView(v),
289
- className: `px-3 py-1 text-xs font-medium rounded-md transition-colors ${view === v ? "bg-blue-600 text-white" : "bg-white border border-gray-300 text-gray-600 hover:bg-gray-50"}`,
290
- children: v.charAt(0).toUpperCase() + v.slice(1)
291
- },
292
- v
293
- )) })
294
- ] })
295
- ] }),
296
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children: view === "year" ? /* @__PURE__ */ jsx(
297
- YearView,
298
- {
299
- year: currentDate.getFullYear(),
300
- eventsByDate,
301
- today: toDateStr(today),
302
- onPickMonth: (m) => {
303
- setCurrentDate(new Date(currentDate.getFullYear(), m, 1));
304
- setView("month");
305
- },
306
- onDayClick: handleDayClick
307
- }
308
- ) : view === "month" ? /* @__PURE__ */ jsx(
309
- MonthView,
310
- {
311
- year: currentDate.getFullYear(),
312
- month: currentDate.getMonth(),
313
- eventsByDate,
314
- today: toDateStr(today),
315
- onDayClick: handleDayClick,
316
- onEventClick: setEditingEvent,
317
- onToggleTodo: toggleTodoDone
318
- }
319
- ) : /* @__PURE__ */ jsx(
320
- WeekView,
321
- {
322
- currentDate,
323
- eventsByDate,
324
- today: toDateStr(today),
325
- onDayClick: handleDayClick,
326
- onEventClick: setEditingEvent,
327
- onToggleTodo: toggleTodoDone
328
- }
329
- ) }),
330
- editingEvent && /* @__PURE__ */ jsx(
331
- EventEditor,
332
- {
333
- event: editingEvent,
334
- isNew: !!newEventDate,
335
- calendars,
336
- onSave: saveEvent,
337
- onDelete: () => deleteEvent(editingEvent),
338
- onClose: () => {
339
- setEditingEvent(null);
340
- setNewEventDate(null);
341
- }
342
- }
343
- )
344
- ] });
345
- }
346
- function extractError(err) {
347
- const r = err?.response?.data?.error;
348
- if (r) return r;
349
- if (err instanceof Error) return err.message;
350
- return "Unknown error";
351
- }
352
- function DayEventBadge({ evt, onEventClick, onToggleTodo, compact = false }) {
353
- if (evt._todo && evt._todoId) {
354
- return compact ? /* @__PURE__ */ jsxs(
355
- "button",
356
- {
357
- onClick: (ev) => {
358
- ev.stopPropagation();
359
- onToggleTodo(evt._todoId);
360
- },
361
- title: evt.title,
362
- className: `w-full text-left flex items-center gap-1 truncate rounded px-1 py-0.5 text-[10px] leading-tight font-medium hover:bg-gray-100 transition-colors ${evt._done ? "text-gray-400" : "text-gray-700"}`,
363
- children: [
364
- /* @__PURE__ */ jsx("span", { className: "shrink-0", children: evt._done ? "\u2611" : "\u2610" }),
365
- /* @__PURE__ */ jsx("span", { className: `truncate ${evt._done ? "line-through" : ""}`, children: evt.title || "Task" })
366
- ]
367
- }
368
- ) : /* @__PURE__ */ jsxs(
369
- "button",
370
- {
371
- onClick: (ev) => {
372
- ev.stopPropagation();
373
- onToggleTodo(evt._todoId);
374
- },
375
- className: `w-full text-left flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-gray-100 transition-colors ${evt._done ? "text-gray-400" : "text-gray-700"}`,
376
- children: [
377
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-base leading-none", children: evt._done ? "\u2611" : "\u2610" }),
378
- /* @__PURE__ */ jsx("span", { className: `text-xs font-medium truncate ${evt._done ? "line-through" : ""}`, children: evt.title || "Task" })
379
- ]
380
- }
381
- );
382
- }
383
- const c = getColor(evt.color);
384
- return compact ? /* @__PURE__ */ jsxs(
385
- "button",
386
- {
387
- onClick: (ev) => {
388
- ev.stopPropagation();
389
- onEventClick(evt);
390
- },
391
- className: `w-full text-left truncate rounded px-1 py-0.5 text-[10px] leading-tight font-medium ${c.light} hover:opacity-80 transition-opacity`,
392
- children: [
393
- !evt.all_day && evt.start_time && /* @__PURE__ */ jsx("span", { className: "text-[9px] opacity-70 mr-0.5", children: evt.start_time }),
394
- evt.title || "Untitled"
395
- ]
396
- }
397
- ) : /* @__PURE__ */ jsxs(
398
- "button",
399
- {
400
- onClick: (ev) => {
401
- ev.stopPropagation();
402
- onEventClick(evt);
403
- },
404
- className: `w-full text-left rounded-md px-2 py-1.5 ${c.light} hover:opacity-80 transition-opacity`,
405
- children: [
406
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium truncate", children: evt.title || "Untitled" }),
407
- !evt.all_day && evt.start_time && /* @__PURE__ */ jsxs("p", { className: "text-[10px] opacity-70", children: [
408
- evt.start_time,
409
- evt.end_time ? ` - ${evt.end_time}` : ""
410
- ] })
411
- ]
412
- }
413
- );
414
- }
415
- function MonthView({ year, month, eventsByDate, today, onDayClick, onEventClick, onToggleTodo }) {
416
- const cells = useMemo(() => getMonthDays(year, month), [year, month]);
417
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col", children: [
418
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 border-b border-gray-200", children: DAYS.map((d) => /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-[10px] font-semibold text-gray-500 uppercase text-center", children: d }, d)) }),
419
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 flex-1 auto-rows-fr", children: cells.map((cell, i) => {
420
- const dateStr = toDateStr(cell.date);
421
- const isToday = dateStr === today;
422
- const dayEvents = eventsByDate[dateStr] || [];
423
- return /* @__PURE__ */ jsxs(
424
- "div",
425
- {
426
- onClick: () => onDayClick(dateStr),
427
- className: `border-b border-r border-gray-100 px-1 py-0.5 cursor-pointer hover:bg-blue-50/50 transition-colors overflow-hidden ${!cell.isCurrentMonth ? "bg-gray-50/50" : ""}`,
428
- children: [
429
- /* @__PURE__ */ jsx("div", { className: `text-[11px] font-medium mb-0.5 w-5 h-5 flex items-center justify-center rounded-full ${isToday ? "bg-blue-600 text-white" : cell.isCurrentMonth ? "text-gray-900" : "text-gray-400"}`, children: cell.date.getDate() }),
430
- /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
431
- dayEvents.slice(0, 3).map((evt) => /* @__PURE__ */ jsx(DayEventBadge, { evt, onEventClick, onToggleTodo, compact: true }, evt.id)),
432
- dayEvents.length > 3 && /* @__PURE__ */ jsxs("p", { className: "text-[9px] text-gray-400 pl-1", children: [
433
- "+",
434
- dayEvents.length - 3,
435
- " more"
436
- ] })
437
- ] })
438
- ]
439
- },
440
- i
441
- );
442
- }) })
443
- ] });
444
- }
445
- function YearView({ year, eventsByDate, today, onPickMonth, onDayClick }) {
446
- const monthShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
447
- const dowShort = ["S", "M", "T", "W", "T", "F", "S"];
448
- return /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto p-4", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-5 max-w-5xl mx-auto", children: monthShort.map((label, m) => {
449
- const cells = getMonthDays(year, m);
450
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
451
- /* @__PURE__ */ jsx(
452
- "button",
453
- {
454
- onClick: () => onPickMonth(m),
455
- className: "self-start text-[13px] font-semibold text-blue-600 hover:text-blue-800 mb-1 px-1 -ml-1 rounded transition-colors",
456
- children: label
457
- }
458
- ),
459
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 mb-0.5", children: dowShort.map((d, i) => /* @__PURE__ */ jsx("div", { className: "text-[9px] font-medium text-gray-400 text-center", children: d }, i)) }),
460
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7", children: cells.map((cell, i) => {
461
- const dateStr = toDateStr(cell.date);
462
- const isToday = dateStr === today;
463
- const hasEvents = !!eventsByDate[dateStr]?.length;
464
- return /* @__PURE__ */ jsxs(
465
- "button",
466
- {
467
- onClick: (e) => {
468
- e.stopPropagation();
469
- onDayClick(dateStr);
470
- },
471
- className: "relative h-6 flex items-center justify-center group",
472
- tabIndex: cell.isCurrentMonth ? 0 : -1,
473
- children: [
474
- /* @__PURE__ */ jsx("span", { className: `text-[10px] tabular-nums leading-none flex items-center justify-center w-5 h-5 rounded-full transition-colors
475
- ${isToday ? "bg-blue-600 text-white font-semibold" : cell.isCurrentMonth ? "text-gray-700 group-hover:bg-blue-100" : "text-gray-300"}`, children: cell.date.getDate() }),
476
- hasEvents && cell.isCurrentMonth && !isToday && /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-blue-500" })
477
- ]
478
- },
479
- i
480
- );
481
- }) })
482
- ] }, m);
483
- }) }) });
484
- }
485
- function WeekView({ currentDate, eventsByDate, today, onDayClick, onEventClick, onToggleTodo }) {
486
- const days = useMemo(() => getWeekDays(currentDate), [currentDate]);
487
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col", children: [
488
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 border-b border-gray-200", children: days.map((d) => {
489
- const dateStr = toDateStr(d);
490
- const isToday = dateStr === today;
491
- return /* @__PURE__ */ jsxs("div", { className: "px-2 py-2 text-center", children: [
492
- /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-gray-500 uppercase", children: DAYS[d.getDay()] }),
493
- /* @__PURE__ */ jsx("p", { className: `text-lg font-semibold mt-0.5 ${isToday ? "text-blue-600" : "text-gray-900"}`, children: d.getDate() })
494
- ] }, dateStr);
495
- }) }),
496
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 flex-1 overflow-y-auto", children: days.map((d) => {
497
- const dateStr = toDateStr(d);
498
- const dayEvents = eventsByDate[dateStr] || [];
499
- return /* @__PURE__ */ jsx(
500
- "div",
501
- {
502
- onClick: () => onDayClick(dateStr),
503
- className: "border-r border-gray-100 px-1.5 py-2 cursor-pointer hover:bg-blue-50/30 transition-colors min-h-[200px]",
504
- children: /* @__PURE__ */ jsx("div", { className: "space-y-1", children: dayEvents.map((evt) => /* @__PURE__ */ jsx(DayEventBadge, { evt, onEventClick, onToggleTodo }, evt.id)) })
505
- },
506
- dateStr
507
- );
508
- }) })
509
- ] });
510
- }
511
- function EventEditor({ event, isNew, calendars, onSave, onDelete, onClose }) {
512
- const [title, setTitle] = useState(event.title);
513
- const [date, setDate] = useState(event.date);
514
- const [startTime, setStartTime] = useState(event.start_time || "09:00");
515
- const [endTime, setEndTime] = useState(event.end_time || "10:00");
516
- const [color, setColor] = useState(event.color);
517
- const [allDay, setAllDay] = useState(event.all_day ?? false);
518
- const [description, setDescription] = useState(event.description || "");
519
- const isFromCaldav = !!event._caldav;
520
- const [target, setTarget] = useState(isFromCaldav ? event._caldav.calendarId : "local");
521
- const handleSave = () => {
522
- if (!title.trim()) {
523
- toast_default.error("Event title is required.");
524
- return;
525
- }
526
- const updated = {
527
- ...event,
528
- title: title.trim(),
529
- date,
530
- start_time: allDay ? void 0 : startTime,
531
- end_time: allDay ? void 0 : endTime,
532
- color,
533
- all_day: allDay,
534
- description: description.trim() || void 0
535
- };
536
- const targetCal = !isFromCaldav && target !== "local" ? target : void 0;
537
- onSave(updated, targetCal);
538
- };
539
- const inp = "block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm";
540
- return /* @__PURE__ */ jsxs(Modal, { open: true, onClose, title: isNew ? "New Event" : "Edit Event", size: "sm", children: [
541
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
542
- /* @__PURE__ */ jsxs("div", { children: [
543
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Title *" }),
544
- /* @__PURE__ */ jsx("input", { value: title, onChange: (e) => setTitle(e.target.value), className: inp, placeholder: "Event title", autoFocus: true })
545
- ] }),
546
- /* @__PURE__ */ jsxs("div", { children: [
547
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Date" }),
548
- /* @__PURE__ */ jsx("input", { type: "date", value: date, onChange: (e) => setDate(e.target.value), className: inp })
549
- ] }),
550
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
551
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: allDay, onChange: (e) => setAllDay(e.target.checked), className: "h-4 w-4 rounded border-gray-300 text-blue-600" }),
552
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700", children: "All day" })
553
- ] }),
554
- !allDay && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
555
- /* @__PURE__ */ jsxs("div", { children: [
556
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Start" }),
557
- /* @__PURE__ */ jsx("input", { type: "time", value: startTime, onChange: (e) => setStartTime(e.target.value), className: inp })
558
- ] }),
559
- /* @__PURE__ */ jsxs("div", { children: [
560
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "End" }),
561
- /* @__PURE__ */ jsx("input", { type: "time", value: endTime, onChange: (e) => setEndTime(e.target.value), className: inp })
562
- ] })
563
- ] }),
564
- isNew && calendars.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
565
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Save to" }),
566
- /* @__PURE__ */ jsxs("select", { value: target, onChange: (e) => setTarget(e.target.value), className: inp, children: [
567
- /* @__PURE__ */ jsx("option", { value: "local", children: "Local (this device only)" }),
568
- calendars.map((c) => /* @__PURE__ */ jsx("option", { value: c.id, children: c.displayName }, c.id))
569
- ] })
570
- ] }),
571
- isFromCaldav && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-gray-500", children: [
572
- "From ",
573
- calendars.find((c) => c.id === event._caldav?.calendarId)?.displayName || "CalDAV"
574
- ] }),
575
- /* @__PURE__ */ jsxs("div", { children: [
576
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Color" }),
577
- /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: COLORS.map((c) => /* @__PURE__ */ jsx(
578
- "button",
579
- {
580
- onClick: () => setColor(c.key),
581
- className: `w-6 h-6 rounded-full ${c.bg} border-2 transition-all ${color === c.key ? "border-gray-700 scale-110" : "border-transparent hover:border-gray-400"}`
582
- },
583
- c.key
584
- )) })
585
- ] }),
586
- /* @__PURE__ */ jsxs("div", { children: [
587
- /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Description" }),
588
- /* @__PURE__ */ jsx("textarea", { value: description, onChange: (e) => setDescription(e.target.value), rows: 2, className: inp, placeholder: "Optional notes..." })
589
- ] })
590
- ] }),
591
- !isNew && /* @__PURE__ */ jsx(ModalActions, { position: "left", children: /* @__PURE__ */ jsx("button", { onClick: onDelete, className: "text-sm text-red-600 hover:text-red-800 font-medium", children: "Delete" }) }),
592
- /* @__PURE__ */ jsx(ModalActions, { children: /* @__PURE__ */ jsx("button", { onClick: handleSave, className: "bg-blue-600 text-white px-4 py-2 text-sm font-medium rounded-lg hover:bg-blue-700", children: isNew ? "Create" : "Save" }) })
593
- ] });
594
- }
595
-
596
- export { Calendar as default };
597
- //# sourceMappingURL=Calendar-QQYHT57Z.js.map
598
- //# sourceMappingURL=Calendar-QQYHT57Z.js.map