@orchestra-mcp/tracking 1.0.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/LICENSE +9 -0
- package/README.md +13 -0
- package/dist/index.cjs +345 -0
- package/dist/index.css +309 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +317 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orchestra MCP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FullCalendar: () => FullCalendar,
|
|
24
|
+
Timer: () => Timer
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/Timer/Timer.tsx
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
function formatTime(totalSeconds, compact) {
|
|
32
|
+
const hrs = Math.floor(totalSeconds / 3600);
|
|
33
|
+
const mins = Math.floor(totalSeconds % 3600 / 60);
|
|
34
|
+
const secs = totalSeconds % 60;
|
|
35
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36
|
+
if (compact && hrs === 0) {
|
|
37
|
+
return `${pad(mins)}:${pad(secs)}`;
|
|
38
|
+
}
|
|
39
|
+
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
|
40
|
+
}
|
|
41
|
+
var Timer = ({
|
|
42
|
+
mode = "stopwatch",
|
|
43
|
+
initialTime = 0,
|
|
44
|
+
autoStart = false,
|
|
45
|
+
display = "full",
|
|
46
|
+
onTick,
|
|
47
|
+
onComplete,
|
|
48
|
+
showControls = true,
|
|
49
|
+
showLaps = false
|
|
50
|
+
}) => {
|
|
51
|
+
const [elapsed, setElapsed] = (0, import_react.useState)(0);
|
|
52
|
+
const [running, setRunning] = (0, import_react.useState)(autoStart);
|
|
53
|
+
const [laps, setLaps] = (0, import_react.useState)([]);
|
|
54
|
+
const intervalRef = (0, import_react.useRef)(null);
|
|
55
|
+
const elapsedRef = (0, import_react.useRef)(0);
|
|
56
|
+
const currentTime = mode === "countdown" ? Math.max(initialTime - elapsed, 0) : elapsed;
|
|
57
|
+
const clearTimer = (0, import_react.useCallback)(() => {
|
|
58
|
+
if (intervalRef.current !== null) {
|
|
59
|
+
clearInterval(intervalRef.current);
|
|
60
|
+
intervalRef.current = null;
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
const startTimer = (0, import_react.useCallback)(() => {
|
|
64
|
+
clearTimer();
|
|
65
|
+
intervalRef.current = setInterval(() => {
|
|
66
|
+
elapsedRef.current += 1;
|
|
67
|
+
setElapsed(elapsedRef.current);
|
|
68
|
+
}, 1e3);
|
|
69
|
+
setRunning(true);
|
|
70
|
+
}, [clearTimer]);
|
|
71
|
+
const pauseTimer = (0, import_react.useCallback)(() => {
|
|
72
|
+
clearTimer();
|
|
73
|
+
setRunning(false);
|
|
74
|
+
}, [clearTimer]);
|
|
75
|
+
const resetTimer = (0, import_react.useCallback)(() => {
|
|
76
|
+
clearTimer();
|
|
77
|
+
setElapsed(0);
|
|
78
|
+
elapsedRef.current = 0;
|
|
79
|
+
setRunning(false);
|
|
80
|
+
setLaps([]);
|
|
81
|
+
}, [clearTimer]);
|
|
82
|
+
const addLap = (0, import_react.useCallback)(() => {
|
|
83
|
+
setLaps((prev) => [...prev, currentTime]);
|
|
84
|
+
}, [currentTime]);
|
|
85
|
+
(0, import_react.useEffect)(() => {
|
|
86
|
+
if (elapsed > 0) onTick?.(elapsed);
|
|
87
|
+
}, [elapsed, onTick]);
|
|
88
|
+
(0, import_react.useEffect)(() => {
|
|
89
|
+
if (mode === "countdown" && currentTime === 0 && elapsed > 0) {
|
|
90
|
+
pauseTimer();
|
|
91
|
+
onComplete?.();
|
|
92
|
+
}
|
|
93
|
+
}, [mode, currentTime, elapsed, pauseTimer, onComplete]);
|
|
94
|
+
(0, import_react.useEffect)(() => {
|
|
95
|
+
if (autoStart) startTimer();
|
|
96
|
+
return clearTimer;
|
|
97
|
+
}, []);
|
|
98
|
+
const compact = display === "compact";
|
|
99
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "timer", "data-mode": mode, children: [
|
|
100
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `timer__display ${compact ? "timer__display--compact" : ""}`, children: formatTime(currentTime, compact) }),
|
|
101
|
+
showControls && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "timer__controls", children: [
|
|
102
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
103
|
+
"button",
|
|
104
|
+
{
|
|
105
|
+
type: "button",
|
|
106
|
+
className: "timer__btn timer__btn--primary",
|
|
107
|
+
onClick: running ? pauseTimer : startTimer,
|
|
108
|
+
children: running ? "Pause" : "Start"
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
112
|
+
"button",
|
|
113
|
+
{
|
|
114
|
+
type: "button",
|
|
115
|
+
className: "timer__btn timer__btn--secondary",
|
|
116
|
+
onClick: resetTimer,
|
|
117
|
+
children: "Reset"
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
showLaps && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
className: "timer__btn timer__btn--secondary",
|
|
125
|
+
onClick: addLap,
|
|
126
|
+
disabled: !running,
|
|
127
|
+
children: "Lap"
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
] }),
|
|
131
|
+
showLaps && laps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "timer__laps", children: laps.map((lap, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: "timer__lap", children: [
|
|
132
|
+
"Lap ",
|
|
133
|
+
i + 1,
|
|
134
|
+
": ",
|
|
135
|
+
formatTime(lap, compact)
|
|
136
|
+
] }, i)) })
|
|
137
|
+
] });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/FullCalendar/FullCalendar.tsx
|
|
141
|
+
var import_react2 = require("react");
|
|
142
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
143
|
+
var DAY_HEADERS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
144
|
+
var DEFAULT_EVENT_COLOR = "#3b82f6";
|
|
145
|
+
function getDaysInMonth(year, month) {
|
|
146
|
+
return new Date(year, month + 1, 0).getDate();
|
|
147
|
+
}
|
|
148
|
+
function getFirstDayOfWeek(year, month) {
|
|
149
|
+
return new Date(year, month, 1).getDay();
|
|
150
|
+
}
|
|
151
|
+
function isSameDay(a, b) {
|
|
152
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
153
|
+
}
|
|
154
|
+
function formatMonthYear(year, month) {
|
|
155
|
+
const date = new Date(year, month, 1);
|
|
156
|
+
return date.toLocaleDateString("en-US", { month: "long", year: "numeric" });
|
|
157
|
+
}
|
|
158
|
+
function buildCalendarGrid(year, month, today) {
|
|
159
|
+
const daysInMonth = getDaysInMonth(year, month);
|
|
160
|
+
const firstDay = getFirstDayOfWeek(year, month);
|
|
161
|
+
const cells = [];
|
|
162
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
163
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
164
|
+
const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth);
|
|
165
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
166
|
+
const day = daysInPrevMonth - i;
|
|
167
|
+
const date = new Date(prevYear, prevMonth, day);
|
|
168
|
+
cells.push({
|
|
169
|
+
date,
|
|
170
|
+
day,
|
|
171
|
+
isCurrentMonth: false,
|
|
172
|
+
isToday: isSameDay(date, today),
|
|
173
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
177
|
+
const date = new Date(year, month, day);
|
|
178
|
+
cells.push({
|
|
179
|
+
date,
|
|
180
|
+
day,
|
|
181
|
+
isCurrentMonth: true,
|
|
182
|
+
isToday: isSameDay(date, today),
|
|
183
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const totalCells = Math.ceil(cells.length / 7) * 7;
|
|
187
|
+
const nextMonth = month === 11 ? 0 : month + 1;
|
|
188
|
+
const nextYear = month === 11 ? year + 1 : year;
|
|
189
|
+
let nextDay = 1;
|
|
190
|
+
while (cells.length < totalCells) {
|
|
191
|
+
const date = new Date(nextYear, nextMonth, nextDay);
|
|
192
|
+
cells.push({
|
|
193
|
+
date,
|
|
194
|
+
day: nextDay,
|
|
195
|
+
isCurrentMonth: false,
|
|
196
|
+
isToday: isSameDay(date, today),
|
|
197
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
198
|
+
});
|
|
199
|
+
nextDay++;
|
|
200
|
+
}
|
|
201
|
+
return cells;
|
|
202
|
+
}
|
|
203
|
+
function dateToKey(date) {
|
|
204
|
+
const y = date.getFullYear();
|
|
205
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
206
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
207
|
+
return `${y}-${m}-${d}`;
|
|
208
|
+
}
|
|
209
|
+
var FullCalendar = ({
|
|
210
|
+
events = [],
|
|
211
|
+
onDateSelect,
|
|
212
|
+
onEventClick,
|
|
213
|
+
selectedDate,
|
|
214
|
+
onMonthChange,
|
|
215
|
+
initialYear,
|
|
216
|
+
initialMonth
|
|
217
|
+
}) => {
|
|
218
|
+
const now = /* @__PURE__ */ new Date();
|
|
219
|
+
const [year, setYear] = (0, import_react2.useState)(initialYear ?? now.getFullYear());
|
|
220
|
+
const [month, setMonth] = (0, import_react2.useState)(initialMonth ?? now.getMonth());
|
|
221
|
+
const today = (0, import_react2.useMemo)(() => /* @__PURE__ */ new Date(), []);
|
|
222
|
+
const cells = (0, import_react2.useMemo)(() => buildCalendarGrid(year, month, today), [year, month, today]);
|
|
223
|
+
const eventsByDate = (0, import_react2.useMemo)(() => {
|
|
224
|
+
const map = {};
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
const key = event.date;
|
|
227
|
+
if (!map[key]) {
|
|
228
|
+
map[key] = [];
|
|
229
|
+
}
|
|
230
|
+
map[key].push(event);
|
|
231
|
+
}
|
|
232
|
+
return map;
|
|
233
|
+
}, [events]);
|
|
234
|
+
const goToPrevMonth = (0, import_react2.useCallback)(() => {
|
|
235
|
+
setYear((y) => month === 0 ? y - 1 : y);
|
|
236
|
+
setMonth((m) => m === 0 ? 11 : m - 1);
|
|
237
|
+
const newMonth = month === 0 ? 11 : month - 1;
|
|
238
|
+
const newYear = month === 0 ? year - 1 : year;
|
|
239
|
+
onMonthChange?.(newYear, newMonth);
|
|
240
|
+
}, [month, year, onMonthChange]);
|
|
241
|
+
const goToNextMonth = (0, import_react2.useCallback)(() => {
|
|
242
|
+
setYear((y) => month === 11 ? y + 1 : y);
|
|
243
|
+
setMonth((m) => m === 11 ? 0 : m + 1);
|
|
244
|
+
const newMonth = month === 11 ? 0 : month + 1;
|
|
245
|
+
const newYear = month === 11 ? year + 1 : year;
|
|
246
|
+
onMonthChange?.(newYear, newMonth);
|
|
247
|
+
}, [month, year, onMonthChange]);
|
|
248
|
+
const handleDateClick = (0, import_react2.useCallback)(
|
|
249
|
+
(date) => {
|
|
250
|
+
onDateSelect?.(date);
|
|
251
|
+
},
|
|
252
|
+
[onDateSelect]
|
|
253
|
+
);
|
|
254
|
+
const handleEventClick = (0, import_react2.useCallback)(
|
|
255
|
+
(event, e) => {
|
|
256
|
+
e.stopPropagation();
|
|
257
|
+
onEventClick?.(event);
|
|
258
|
+
},
|
|
259
|
+
[onEventClick]
|
|
260
|
+
);
|
|
261
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "calendar", "data-testid": "full-calendar", children: [
|
|
262
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "calendar__header", children: [
|
|
263
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "calendar__title", "data-testid": "calendar-title", children: formatMonthYear(year, month) }),
|
|
264
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "calendar__nav", children: [
|
|
265
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
266
|
+
"button",
|
|
267
|
+
{
|
|
268
|
+
type: "button",
|
|
269
|
+
className: "calendar__nav-btn",
|
|
270
|
+
onClick: goToPrevMonth,
|
|
271
|
+
"aria-label": "Previous month",
|
|
272
|
+
children: "\u2039"
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
276
|
+
"button",
|
|
277
|
+
{
|
|
278
|
+
type: "button",
|
|
279
|
+
className: "calendar__nav-btn",
|
|
280
|
+
onClick: goToNextMonth,
|
|
281
|
+
"aria-label": "Next month",
|
|
282
|
+
children: "\u203A"
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
] })
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "calendar__grid", role: "grid", "aria-label": "Calendar", children: [
|
|
288
|
+
DAY_HEADERS.map((day) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
289
|
+
"div",
|
|
290
|
+
{
|
|
291
|
+
className: "calendar__day-header",
|
|
292
|
+
role: "columnheader",
|
|
293
|
+
children: day
|
|
294
|
+
},
|
|
295
|
+
day
|
|
296
|
+
)),
|
|
297
|
+
cells.map((cell) => {
|
|
298
|
+
const key = dateToKey(cell.date);
|
|
299
|
+
const cellEvents = eventsByDate[key] || [];
|
|
300
|
+
const isSelected = selectedDate ? isSameDay(cell.date, selectedDate) : false;
|
|
301
|
+
const classNames = ["calendar__cell"];
|
|
302
|
+
if (!cell.isCurrentMonth) classNames.push("calendar__cell--outside");
|
|
303
|
+
if (cell.isWeekend) classNames.push("calendar__cell--weekend");
|
|
304
|
+
if (cell.isToday) classNames.push("calendar__cell--today");
|
|
305
|
+
if (isSelected) classNames.push("calendar__cell--selected");
|
|
306
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
307
|
+
"div",
|
|
308
|
+
{
|
|
309
|
+
className: classNames.join(" "),
|
|
310
|
+
role: "gridcell",
|
|
311
|
+
"aria-label": cell.date.toLocaleDateString("en-US", {
|
|
312
|
+
month: "long",
|
|
313
|
+
day: "numeric",
|
|
314
|
+
year: "numeric"
|
|
315
|
+
}),
|
|
316
|
+
onClick: () => handleDateClick(cell.date),
|
|
317
|
+
children: [
|
|
318
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "calendar__date", children: cell.day }),
|
|
319
|
+
cellEvents.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "calendar__events", children: cellEvents.map((event) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
320
|
+
"div",
|
|
321
|
+
{
|
|
322
|
+
className: "calendar__event",
|
|
323
|
+
style: {
|
|
324
|
+
backgroundColor: event.color || DEFAULT_EVENT_COLOR
|
|
325
|
+
},
|
|
326
|
+
title: event.title,
|
|
327
|
+
"data-testid": `event-${event.id}`,
|
|
328
|
+
onClick: (e) => handleEventClick(event, e),
|
|
329
|
+
children: event.title
|
|
330
|
+
},
|
|
331
|
+
event.id
|
|
332
|
+
)) })
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
`${cell.isCurrentMonth ? "cur" : "out"}-${key}`
|
|
336
|
+
);
|
|
337
|
+
})
|
|
338
|
+
] })
|
|
339
|
+
] });
|
|
340
|
+
};
|
|
341
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
342
|
+
0 && (module.exports = {
|
|
343
|
+
FullCalendar,
|
|
344
|
+
Timer
|
|
345
|
+
});
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/* src/Timer/Timer.css */
|
|
2
|
+
.timer {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 12px;
|
|
7
|
+
font-family:
|
|
8
|
+
system-ui,
|
|
9
|
+
-apple-system,
|
|
10
|
+
sans-serif;
|
|
11
|
+
}
|
|
12
|
+
.timer__display {
|
|
13
|
+
font-size: 36px;
|
|
14
|
+
font-weight: 600;
|
|
15
|
+
font-variant-numeric: tabular-nums;
|
|
16
|
+
color: var(--color-fg);
|
|
17
|
+
background: var(--color-bg-alt);
|
|
18
|
+
border: 1px solid var(--color-border);
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
padding: 16px 32px;
|
|
21
|
+
min-width: 200px;
|
|
22
|
+
text-align: center;
|
|
23
|
+
}
|
|
24
|
+
.timer__display--compact {
|
|
25
|
+
font-size: 24px;
|
|
26
|
+
padding: 8px 16px;
|
|
27
|
+
min-width: 120px;
|
|
28
|
+
}
|
|
29
|
+
.timer__controls {
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: 8px;
|
|
32
|
+
}
|
|
33
|
+
.timer__btn {
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
font-size: 14px;
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
border: none;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
padding: 8px 16px;
|
|
41
|
+
transition: all 0.15s ease;
|
|
42
|
+
}
|
|
43
|
+
.timer__btn--primary {
|
|
44
|
+
background-color: var(--color-accent);
|
|
45
|
+
color: #ffffff;
|
|
46
|
+
}
|
|
47
|
+
.timer__btn--primary:hover {
|
|
48
|
+
opacity: 0.85;
|
|
49
|
+
}
|
|
50
|
+
.timer__btn--secondary {
|
|
51
|
+
background-color: var(--color-bg-alt);
|
|
52
|
+
color: var(--color-fg);
|
|
53
|
+
border: 1px solid var(--color-border);
|
|
54
|
+
}
|
|
55
|
+
.timer__btn--secondary:hover:not(:disabled) {
|
|
56
|
+
border-color: var(--color-accent);
|
|
57
|
+
}
|
|
58
|
+
.timer__btn:disabled {
|
|
59
|
+
opacity: 0.5;
|
|
60
|
+
cursor: not-allowed;
|
|
61
|
+
}
|
|
62
|
+
.timer__laps {
|
|
63
|
+
list-style: none;
|
|
64
|
+
padding: 0;
|
|
65
|
+
margin: 8px 0 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
max-width: 280px;
|
|
68
|
+
}
|
|
69
|
+
.timer__lap {
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
color: var(--color-fg);
|
|
72
|
+
padding: 6px 12px;
|
|
73
|
+
border-bottom: 1px solid var(--color-border);
|
|
74
|
+
font-variant-numeric: tabular-nums;
|
|
75
|
+
}
|
|
76
|
+
.timer__lap:last-child {
|
|
77
|
+
border-bottom: none;
|
|
78
|
+
}
|
|
79
|
+
[data-variant=compact] .timer__display {
|
|
80
|
+
font-size: 24px;
|
|
81
|
+
padding: 8px 16px;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
}
|
|
84
|
+
[data-variant=compact] .timer__display--compact {
|
|
85
|
+
font-size: 18px;
|
|
86
|
+
padding: 6px 12px;
|
|
87
|
+
}
|
|
88
|
+
[data-variant=compact] .timer__btn {
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
padding: 6px 12px;
|
|
91
|
+
border-radius: 4px;
|
|
92
|
+
}
|
|
93
|
+
[data-variant=modern] .timer__display {
|
|
94
|
+
font-size: 42px;
|
|
95
|
+
font-weight: 700;
|
|
96
|
+
padding: 20px 40px;
|
|
97
|
+
border-radius: 12px;
|
|
98
|
+
}
|
|
99
|
+
[data-variant=modern] .timer__display--compact {
|
|
100
|
+
font-size: 28px;
|
|
101
|
+
padding: 12px 24px;
|
|
102
|
+
}
|
|
103
|
+
[data-variant=modern] .timer__btn {
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
padding: 10px 20px;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* src/FullCalendar/FullCalendar.css */
|
|
111
|
+
.calendar {
|
|
112
|
+
font-family:
|
|
113
|
+
system-ui,
|
|
114
|
+
-apple-system,
|
|
115
|
+
sans-serif;
|
|
116
|
+
background-color: var(--color-bg);
|
|
117
|
+
color: var(--color-fg);
|
|
118
|
+
border: 1px solid var(--color-border);
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
.calendar__header {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
padding: 12px 16px;
|
|
128
|
+
background-color: var(--color-bg-alt);
|
|
129
|
+
border-bottom: 1px solid var(--color-border);
|
|
130
|
+
}
|
|
131
|
+
.calendar__title {
|
|
132
|
+
font-size: 16px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: var(--color-fg);
|
|
135
|
+
margin: 0;
|
|
136
|
+
}
|
|
137
|
+
.calendar__nav {
|
|
138
|
+
display: flex;
|
|
139
|
+
gap: 4px;
|
|
140
|
+
}
|
|
141
|
+
.calendar__nav-btn {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
width: 32px;
|
|
146
|
+
height: 32px;
|
|
147
|
+
border: 1px solid var(--color-border);
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
background-color: var(--color-bg);
|
|
150
|
+
color: var(--color-fg);
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
font-size: 14px;
|
|
153
|
+
transition: all 0.15s ease;
|
|
154
|
+
}
|
|
155
|
+
.calendar__nav-btn:hover {
|
|
156
|
+
background-color: var(--color-bg-active);
|
|
157
|
+
border-color: var(--color-accent);
|
|
158
|
+
}
|
|
159
|
+
.calendar__grid {
|
|
160
|
+
display: grid;
|
|
161
|
+
grid-template-columns: repeat(7, 1fr);
|
|
162
|
+
}
|
|
163
|
+
.calendar__day-header {
|
|
164
|
+
padding: 8px 4px;
|
|
165
|
+
text-align: center;
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
color: var(--color-fg-muted);
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
letter-spacing: 0.5px;
|
|
171
|
+
border-bottom: 1px solid var(--color-border);
|
|
172
|
+
}
|
|
173
|
+
.calendar__cell {
|
|
174
|
+
min-height: 80px;
|
|
175
|
+
padding: 4px;
|
|
176
|
+
border-right: 1px solid var(--color-border);
|
|
177
|
+
border-bottom: 1px solid var(--color-border);
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: background-color 0.15s ease;
|
|
180
|
+
position: relative;
|
|
181
|
+
}
|
|
182
|
+
.calendar__cell:nth-child(7n) {
|
|
183
|
+
border-right: none;
|
|
184
|
+
}
|
|
185
|
+
.calendar__cell:hover {
|
|
186
|
+
background-color: var(--color-bg-active);
|
|
187
|
+
}
|
|
188
|
+
.calendar__cell--outside {
|
|
189
|
+
opacity: 0.35;
|
|
190
|
+
}
|
|
191
|
+
.calendar__cell--weekend {
|
|
192
|
+
background-color: color-mix(in srgb, var(--color-bg-alt) 50%, transparent);
|
|
193
|
+
}
|
|
194
|
+
.calendar__cell--today {
|
|
195
|
+
border: 2px solid var(--color-accent);
|
|
196
|
+
}
|
|
197
|
+
.calendar__cell--selected {
|
|
198
|
+
background-color: color-mix(in srgb, var(--color-accent) 15%, transparent);
|
|
199
|
+
}
|
|
200
|
+
.calendar__date {
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
justify-content: center;
|
|
204
|
+
width: 24px;
|
|
205
|
+
height: 24px;
|
|
206
|
+
font-size: 13px;
|
|
207
|
+
font-weight: 500;
|
|
208
|
+
color: var(--color-fg);
|
|
209
|
+
margin-bottom: 2px;
|
|
210
|
+
}
|
|
211
|
+
.calendar__cell--today .calendar__date {
|
|
212
|
+
background-color: var(--color-accent);
|
|
213
|
+
color: #ffffff;
|
|
214
|
+
border-radius: 50%;
|
|
215
|
+
font-weight: 700;
|
|
216
|
+
}
|
|
217
|
+
.calendar__events {
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 2px;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
}
|
|
223
|
+
.calendar__event {
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
line-height: 1.3;
|
|
226
|
+
padding: 1px 4px;
|
|
227
|
+
border-radius: 3px;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
overflow: hidden;
|
|
230
|
+
text-overflow: ellipsis;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
color: #ffffff;
|
|
233
|
+
font-weight: 500;
|
|
234
|
+
transition: opacity 0.15s ease;
|
|
235
|
+
}
|
|
236
|
+
.calendar__event:hover {
|
|
237
|
+
opacity: 0.8;
|
|
238
|
+
}
|
|
239
|
+
[data-variant=compact] .calendar {
|
|
240
|
+
border-radius: 4px;
|
|
241
|
+
}
|
|
242
|
+
[data-variant=compact] .calendar__header {
|
|
243
|
+
padding: 8px 12px;
|
|
244
|
+
}
|
|
245
|
+
[data-variant=compact] .calendar__title {
|
|
246
|
+
font-size: 14px;
|
|
247
|
+
}
|
|
248
|
+
[data-variant=compact] .calendar__nav-btn {
|
|
249
|
+
width: 26px;
|
|
250
|
+
height: 26px;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
border-radius: 4px;
|
|
253
|
+
}
|
|
254
|
+
[data-variant=compact] .calendar__cell {
|
|
255
|
+
min-height: 56px;
|
|
256
|
+
padding: 2px;
|
|
257
|
+
}
|
|
258
|
+
[data-variant=compact] .calendar__date {
|
|
259
|
+
width: 20px;
|
|
260
|
+
height: 20px;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
}
|
|
263
|
+
[data-variant=compact] .calendar__day-header {
|
|
264
|
+
padding: 4px 2px;
|
|
265
|
+
font-size: 10px;
|
|
266
|
+
}
|
|
267
|
+
[data-variant=compact] .calendar__event {
|
|
268
|
+
font-size: 10px;
|
|
269
|
+
padding: 0 3px;
|
|
270
|
+
}
|
|
271
|
+
[data-variant=modern] .calendar {
|
|
272
|
+
border-radius: 12px;
|
|
273
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
274
|
+
}
|
|
275
|
+
[data-variant=modern] .calendar__header {
|
|
276
|
+
padding: 16px 20px;
|
|
277
|
+
}
|
|
278
|
+
[data-variant=modern] .calendar__title {
|
|
279
|
+
font-size: 18px;
|
|
280
|
+
font-weight: 700;
|
|
281
|
+
}
|
|
282
|
+
[data-variant=modern] .calendar__nav-btn {
|
|
283
|
+
width: 36px;
|
|
284
|
+
height: 36px;
|
|
285
|
+
border-radius: 10px;
|
|
286
|
+
font-size: 16px;
|
|
287
|
+
}
|
|
288
|
+
[data-variant=modern] .calendar__cell {
|
|
289
|
+
min-height: 90px;
|
|
290
|
+
padding: 6px;
|
|
291
|
+
border-radius: 0;
|
|
292
|
+
}
|
|
293
|
+
[data-variant=modern] .calendar__date {
|
|
294
|
+
width: 28px;
|
|
295
|
+
height: 28px;
|
|
296
|
+
font-size: 14px;
|
|
297
|
+
}
|
|
298
|
+
[data-variant=modern] .calendar__cell--today .calendar__date {
|
|
299
|
+
border-radius: 8px;
|
|
300
|
+
}
|
|
301
|
+
[data-variant=modern] .calendar__day-header {
|
|
302
|
+
padding: 10px 4px;
|
|
303
|
+
font-size: 12px;
|
|
304
|
+
}
|
|
305
|
+
[data-variant=modern] .calendar__event {
|
|
306
|
+
font-size: 11px;
|
|
307
|
+
padding: 2px 6px;
|
|
308
|
+
border-radius: 6px;
|
|
309
|
+
}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface TimerProps {
|
|
4
|
+
/** Timer mode */
|
|
5
|
+
mode?: 'stopwatch' | 'countdown';
|
|
6
|
+
/** Initial time in seconds (countdown start, or stopwatch offset) */
|
|
7
|
+
initialTime?: number;
|
|
8
|
+
/** Start timer automatically on mount */
|
|
9
|
+
autoStart?: boolean;
|
|
10
|
+
/** Display format */
|
|
11
|
+
display?: 'full' | 'compact';
|
|
12
|
+
/** Called every second with elapsed seconds */
|
|
13
|
+
onTick?: (seconds: number) => void;
|
|
14
|
+
/** Called when countdown reaches 0 */
|
|
15
|
+
onComplete?: () => void;
|
|
16
|
+
/** Show start/pause/reset controls */
|
|
17
|
+
showControls?: boolean;
|
|
18
|
+
/** Show lap button and lap list */
|
|
19
|
+
showLaps?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare const Timer: ({ mode, initialTime, autoStart, display, onTick, onComplete, showControls, showLaps, }: TimerProps) => react_jsx_runtime.JSX.Element;
|
|
22
|
+
|
|
23
|
+
interface CalendarEvent {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
/** ISO date string (YYYY-MM-DD) */
|
|
27
|
+
date: string;
|
|
28
|
+
color?: string;
|
|
29
|
+
allDay?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface FullCalendarProps {
|
|
32
|
+
/** Array of calendar events to display */
|
|
33
|
+
events?: CalendarEvent[];
|
|
34
|
+
/** Called when a date cell is clicked */
|
|
35
|
+
onDateSelect?: (date: Date) => void;
|
|
36
|
+
/** Called when an event pill is clicked */
|
|
37
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
38
|
+
/** Currently selected date */
|
|
39
|
+
selectedDate?: Date;
|
|
40
|
+
/** Called when month navigation changes */
|
|
41
|
+
onMonthChange?: (year: number, month: number) => void;
|
|
42
|
+
/** Initial year to display (defaults to current year) */
|
|
43
|
+
initialYear?: number;
|
|
44
|
+
/** Initial month to display, 0-indexed (defaults to current month) */
|
|
45
|
+
initialMonth?: number;
|
|
46
|
+
}
|
|
47
|
+
declare const FullCalendar: ({ events, onDateSelect, onEventClick, selectedDate, onMonthChange, initialYear, initialMonth, }: FullCalendarProps) => react_jsx_runtime.JSX.Element;
|
|
48
|
+
|
|
49
|
+
export { type CalendarEvent, FullCalendar, type FullCalendarProps, Timer, type TimerProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface TimerProps {
|
|
4
|
+
/** Timer mode */
|
|
5
|
+
mode?: 'stopwatch' | 'countdown';
|
|
6
|
+
/** Initial time in seconds (countdown start, or stopwatch offset) */
|
|
7
|
+
initialTime?: number;
|
|
8
|
+
/** Start timer automatically on mount */
|
|
9
|
+
autoStart?: boolean;
|
|
10
|
+
/** Display format */
|
|
11
|
+
display?: 'full' | 'compact';
|
|
12
|
+
/** Called every second with elapsed seconds */
|
|
13
|
+
onTick?: (seconds: number) => void;
|
|
14
|
+
/** Called when countdown reaches 0 */
|
|
15
|
+
onComplete?: () => void;
|
|
16
|
+
/** Show start/pause/reset controls */
|
|
17
|
+
showControls?: boolean;
|
|
18
|
+
/** Show lap button and lap list */
|
|
19
|
+
showLaps?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare const Timer: ({ mode, initialTime, autoStart, display, onTick, onComplete, showControls, showLaps, }: TimerProps) => react_jsx_runtime.JSX.Element;
|
|
22
|
+
|
|
23
|
+
interface CalendarEvent {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
/** ISO date string (YYYY-MM-DD) */
|
|
27
|
+
date: string;
|
|
28
|
+
color?: string;
|
|
29
|
+
allDay?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface FullCalendarProps {
|
|
32
|
+
/** Array of calendar events to display */
|
|
33
|
+
events?: CalendarEvent[];
|
|
34
|
+
/** Called when a date cell is clicked */
|
|
35
|
+
onDateSelect?: (date: Date) => void;
|
|
36
|
+
/** Called when an event pill is clicked */
|
|
37
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
38
|
+
/** Currently selected date */
|
|
39
|
+
selectedDate?: Date;
|
|
40
|
+
/** Called when month navigation changes */
|
|
41
|
+
onMonthChange?: (year: number, month: number) => void;
|
|
42
|
+
/** Initial year to display (defaults to current year) */
|
|
43
|
+
initialYear?: number;
|
|
44
|
+
/** Initial month to display, 0-indexed (defaults to current month) */
|
|
45
|
+
initialMonth?: number;
|
|
46
|
+
}
|
|
47
|
+
declare const FullCalendar: ({ events, onDateSelect, onEventClick, selectedDate, onMonthChange, initialYear, initialMonth, }: FullCalendarProps) => react_jsx_runtime.JSX.Element;
|
|
48
|
+
|
|
49
|
+
export { type CalendarEvent, FullCalendar, type FullCalendarProps, Timer, type TimerProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// src/Timer/Timer.tsx
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
function formatTime(totalSeconds, compact) {
|
|
5
|
+
const hrs = Math.floor(totalSeconds / 3600);
|
|
6
|
+
const mins = Math.floor(totalSeconds % 3600 / 60);
|
|
7
|
+
const secs = totalSeconds % 60;
|
|
8
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
9
|
+
if (compact && hrs === 0) {
|
|
10
|
+
return `${pad(mins)}:${pad(secs)}`;
|
|
11
|
+
}
|
|
12
|
+
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
|
13
|
+
}
|
|
14
|
+
var Timer = ({
|
|
15
|
+
mode = "stopwatch",
|
|
16
|
+
initialTime = 0,
|
|
17
|
+
autoStart = false,
|
|
18
|
+
display = "full",
|
|
19
|
+
onTick,
|
|
20
|
+
onComplete,
|
|
21
|
+
showControls = true,
|
|
22
|
+
showLaps = false
|
|
23
|
+
}) => {
|
|
24
|
+
const [elapsed, setElapsed] = useState(0);
|
|
25
|
+
const [running, setRunning] = useState(autoStart);
|
|
26
|
+
const [laps, setLaps] = useState([]);
|
|
27
|
+
const intervalRef = useRef(null);
|
|
28
|
+
const elapsedRef = useRef(0);
|
|
29
|
+
const currentTime = mode === "countdown" ? Math.max(initialTime - elapsed, 0) : elapsed;
|
|
30
|
+
const clearTimer = useCallback(() => {
|
|
31
|
+
if (intervalRef.current !== null) {
|
|
32
|
+
clearInterval(intervalRef.current);
|
|
33
|
+
intervalRef.current = null;
|
|
34
|
+
}
|
|
35
|
+
}, []);
|
|
36
|
+
const startTimer = useCallback(() => {
|
|
37
|
+
clearTimer();
|
|
38
|
+
intervalRef.current = setInterval(() => {
|
|
39
|
+
elapsedRef.current += 1;
|
|
40
|
+
setElapsed(elapsedRef.current);
|
|
41
|
+
}, 1e3);
|
|
42
|
+
setRunning(true);
|
|
43
|
+
}, [clearTimer]);
|
|
44
|
+
const pauseTimer = useCallback(() => {
|
|
45
|
+
clearTimer();
|
|
46
|
+
setRunning(false);
|
|
47
|
+
}, [clearTimer]);
|
|
48
|
+
const resetTimer = useCallback(() => {
|
|
49
|
+
clearTimer();
|
|
50
|
+
setElapsed(0);
|
|
51
|
+
elapsedRef.current = 0;
|
|
52
|
+
setRunning(false);
|
|
53
|
+
setLaps([]);
|
|
54
|
+
}, [clearTimer]);
|
|
55
|
+
const addLap = useCallback(() => {
|
|
56
|
+
setLaps((prev) => [...prev, currentTime]);
|
|
57
|
+
}, [currentTime]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (elapsed > 0) onTick?.(elapsed);
|
|
60
|
+
}, [elapsed, onTick]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (mode === "countdown" && currentTime === 0 && elapsed > 0) {
|
|
63
|
+
pauseTimer();
|
|
64
|
+
onComplete?.();
|
|
65
|
+
}
|
|
66
|
+
}, [mode, currentTime, elapsed, pauseTimer, onComplete]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (autoStart) startTimer();
|
|
69
|
+
return clearTimer;
|
|
70
|
+
}, []);
|
|
71
|
+
const compact = display === "compact";
|
|
72
|
+
return /* @__PURE__ */ jsxs("div", { className: "timer", "data-mode": mode, children: [
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: `timer__display ${compact ? "timer__display--compact" : ""}`, children: formatTime(currentTime, compact) }),
|
|
74
|
+
showControls && /* @__PURE__ */ jsxs("div", { className: "timer__controls", children: [
|
|
75
|
+
/* @__PURE__ */ jsx(
|
|
76
|
+
"button",
|
|
77
|
+
{
|
|
78
|
+
type: "button",
|
|
79
|
+
className: "timer__btn timer__btn--primary",
|
|
80
|
+
onClick: running ? pauseTimer : startTimer,
|
|
81
|
+
children: running ? "Pause" : "Start"
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
type: "button",
|
|
88
|
+
className: "timer__btn timer__btn--secondary",
|
|
89
|
+
onClick: resetTimer,
|
|
90
|
+
children: "Reset"
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
showLaps && /* @__PURE__ */ jsx(
|
|
94
|
+
"button",
|
|
95
|
+
{
|
|
96
|
+
type: "button",
|
|
97
|
+
className: "timer__btn timer__btn--secondary",
|
|
98
|
+
onClick: addLap,
|
|
99
|
+
disabled: !running,
|
|
100
|
+
children: "Lap"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
] }),
|
|
104
|
+
showLaps && laps.length > 0 && /* @__PURE__ */ jsx("ol", { className: "timer__laps", children: laps.map((lap, i) => /* @__PURE__ */ jsxs("li", { className: "timer__lap", children: [
|
|
105
|
+
"Lap ",
|
|
106
|
+
i + 1,
|
|
107
|
+
": ",
|
|
108
|
+
formatTime(lap, compact)
|
|
109
|
+
] }, i)) })
|
|
110
|
+
] });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/FullCalendar/FullCalendar.tsx
|
|
114
|
+
import { useState as useState2, useMemo, useCallback as useCallback2 } from "react";
|
|
115
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
116
|
+
var DAY_HEADERS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
117
|
+
var DEFAULT_EVENT_COLOR = "#3b82f6";
|
|
118
|
+
function getDaysInMonth(year, month) {
|
|
119
|
+
return new Date(year, month + 1, 0).getDate();
|
|
120
|
+
}
|
|
121
|
+
function getFirstDayOfWeek(year, month) {
|
|
122
|
+
return new Date(year, month, 1).getDay();
|
|
123
|
+
}
|
|
124
|
+
function isSameDay(a, b) {
|
|
125
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
126
|
+
}
|
|
127
|
+
function formatMonthYear(year, month) {
|
|
128
|
+
const date = new Date(year, month, 1);
|
|
129
|
+
return date.toLocaleDateString("en-US", { month: "long", year: "numeric" });
|
|
130
|
+
}
|
|
131
|
+
function buildCalendarGrid(year, month, today) {
|
|
132
|
+
const daysInMonth = getDaysInMonth(year, month);
|
|
133
|
+
const firstDay = getFirstDayOfWeek(year, month);
|
|
134
|
+
const cells = [];
|
|
135
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
136
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
137
|
+
const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth);
|
|
138
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
139
|
+
const day = daysInPrevMonth - i;
|
|
140
|
+
const date = new Date(prevYear, prevMonth, day);
|
|
141
|
+
cells.push({
|
|
142
|
+
date,
|
|
143
|
+
day,
|
|
144
|
+
isCurrentMonth: false,
|
|
145
|
+
isToday: isSameDay(date, today),
|
|
146
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
150
|
+
const date = new Date(year, month, day);
|
|
151
|
+
cells.push({
|
|
152
|
+
date,
|
|
153
|
+
day,
|
|
154
|
+
isCurrentMonth: true,
|
|
155
|
+
isToday: isSameDay(date, today),
|
|
156
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const totalCells = Math.ceil(cells.length / 7) * 7;
|
|
160
|
+
const nextMonth = month === 11 ? 0 : month + 1;
|
|
161
|
+
const nextYear = month === 11 ? year + 1 : year;
|
|
162
|
+
let nextDay = 1;
|
|
163
|
+
while (cells.length < totalCells) {
|
|
164
|
+
const date = new Date(nextYear, nextMonth, nextDay);
|
|
165
|
+
cells.push({
|
|
166
|
+
date,
|
|
167
|
+
day: nextDay,
|
|
168
|
+
isCurrentMonth: false,
|
|
169
|
+
isToday: isSameDay(date, today),
|
|
170
|
+
isWeekend: date.getDay() === 0 || date.getDay() === 6
|
|
171
|
+
});
|
|
172
|
+
nextDay++;
|
|
173
|
+
}
|
|
174
|
+
return cells;
|
|
175
|
+
}
|
|
176
|
+
function dateToKey(date) {
|
|
177
|
+
const y = date.getFullYear();
|
|
178
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
179
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
180
|
+
return `${y}-${m}-${d}`;
|
|
181
|
+
}
|
|
182
|
+
var FullCalendar = ({
|
|
183
|
+
events = [],
|
|
184
|
+
onDateSelect,
|
|
185
|
+
onEventClick,
|
|
186
|
+
selectedDate,
|
|
187
|
+
onMonthChange,
|
|
188
|
+
initialYear,
|
|
189
|
+
initialMonth
|
|
190
|
+
}) => {
|
|
191
|
+
const now = /* @__PURE__ */ new Date();
|
|
192
|
+
const [year, setYear] = useState2(initialYear ?? now.getFullYear());
|
|
193
|
+
const [month, setMonth] = useState2(initialMonth ?? now.getMonth());
|
|
194
|
+
const today = useMemo(() => /* @__PURE__ */ new Date(), []);
|
|
195
|
+
const cells = useMemo(() => buildCalendarGrid(year, month, today), [year, month, today]);
|
|
196
|
+
const eventsByDate = useMemo(() => {
|
|
197
|
+
const map = {};
|
|
198
|
+
for (const event of events) {
|
|
199
|
+
const key = event.date;
|
|
200
|
+
if (!map[key]) {
|
|
201
|
+
map[key] = [];
|
|
202
|
+
}
|
|
203
|
+
map[key].push(event);
|
|
204
|
+
}
|
|
205
|
+
return map;
|
|
206
|
+
}, [events]);
|
|
207
|
+
const goToPrevMonth = useCallback2(() => {
|
|
208
|
+
setYear((y) => month === 0 ? y - 1 : y);
|
|
209
|
+
setMonth((m) => m === 0 ? 11 : m - 1);
|
|
210
|
+
const newMonth = month === 0 ? 11 : month - 1;
|
|
211
|
+
const newYear = month === 0 ? year - 1 : year;
|
|
212
|
+
onMonthChange?.(newYear, newMonth);
|
|
213
|
+
}, [month, year, onMonthChange]);
|
|
214
|
+
const goToNextMonth = useCallback2(() => {
|
|
215
|
+
setYear((y) => month === 11 ? y + 1 : y);
|
|
216
|
+
setMonth((m) => m === 11 ? 0 : m + 1);
|
|
217
|
+
const newMonth = month === 11 ? 0 : month + 1;
|
|
218
|
+
const newYear = month === 11 ? year + 1 : year;
|
|
219
|
+
onMonthChange?.(newYear, newMonth);
|
|
220
|
+
}, [month, year, onMonthChange]);
|
|
221
|
+
const handleDateClick = useCallback2(
|
|
222
|
+
(date) => {
|
|
223
|
+
onDateSelect?.(date);
|
|
224
|
+
},
|
|
225
|
+
[onDateSelect]
|
|
226
|
+
);
|
|
227
|
+
const handleEventClick = useCallback2(
|
|
228
|
+
(event, e) => {
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
onEventClick?.(event);
|
|
231
|
+
},
|
|
232
|
+
[onEventClick]
|
|
233
|
+
);
|
|
234
|
+
return /* @__PURE__ */ jsxs2("div", { className: "calendar", "data-testid": "full-calendar", children: [
|
|
235
|
+
/* @__PURE__ */ jsxs2("div", { className: "calendar__header", children: [
|
|
236
|
+
/* @__PURE__ */ jsx2("h2", { className: "calendar__title", "data-testid": "calendar-title", children: formatMonthYear(year, month) }),
|
|
237
|
+
/* @__PURE__ */ jsxs2("div", { className: "calendar__nav", children: [
|
|
238
|
+
/* @__PURE__ */ jsx2(
|
|
239
|
+
"button",
|
|
240
|
+
{
|
|
241
|
+
type: "button",
|
|
242
|
+
className: "calendar__nav-btn",
|
|
243
|
+
onClick: goToPrevMonth,
|
|
244
|
+
"aria-label": "Previous month",
|
|
245
|
+
children: "\u2039"
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
/* @__PURE__ */ jsx2(
|
|
249
|
+
"button",
|
|
250
|
+
{
|
|
251
|
+
type: "button",
|
|
252
|
+
className: "calendar__nav-btn",
|
|
253
|
+
onClick: goToNextMonth,
|
|
254
|
+
"aria-label": "Next month",
|
|
255
|
+
children: "\u203A"
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
] })
|
|
259
|
+
] }),
|
|
260
|
+
/* @__PURE__ */ jsxs2("div", { className: "calendar__grid", role: "grid", "aria-label": "Calendar", children: [
|
|
261
|
+
DAY_HEADERS.map((day) => /* @__PURE__ */ jsx2(
|
|
262
|
+
"div",
|
|
263
|
+
{
|
|
264
|
+
className: "calendar__day-header",
|
|
265
|
+
role: "columnheader",
|
|
266
|
+
children: day
|
|
267
|
+
},
|
|
268
|
+
day
|
|
269
|
+
)),
|
|
270
|
+
cells.map((cell) => {
|
|
271
|
+
const key = dateToKey(cell.date);
|
|
272
|
+
const cellEvents = eventsByDate[key] || [];
|
|
273
|
+
const isSelected = selectedDate ? isSameDay(cell.date, selectedDate) : false;
|
|
274
|
+
const classNames = ["calendar__cell"];
|
|
275
|
+
if (!cell.isCurrentMonth) classNames.push("calendar__cell--outside");
|
|
276
|
+
if (cell.isWeekend) classNames.push("calendar__cell--weekend");
|
|
277
|
+
if (cell.isToday) classNames.push("calendar__cell--today");
|
|
278
|
+
if (isSelected) classNames.push("calendar__cell--selected");
|
|
279
|
+
return /* @__PURE__ */ jsxs2(
|
|
280
|
+
"div",
|
|
281
|
+
{
|
|
282
|
+
className: classNames.join(" "),
|
|
283
|
+
role: "gridcell",
|
|
284
|
+
"aria-label": cell.date.toLocaleDateString("en-US", {
|
|
285
|
+
month: "long",
|
|
286
|
+
day: "numeric",
|
|
287
|
+
year: "numeric"
|
|
288
|
+
}),
|
|
289
|
+
onClick: () => handleDateClick(cell.date),
|
|
290
|
+
children: [
|
|
291
|
+
/* @__PURE__ */ jsx2("span", { className: "calendar__date", children: cell.day }),
|
|
292
|
+
cellEvents.length > 0 && /* @__PURE__ */ jsx2("div", { className: "calendar__events", children: cellEvents.map((event) => /* @__PURE__ */ jsx2(
|
|
293
|
+
"div",
|
|
294
|
+
{
|
|
295
|
+
className: "calendar__event",
|
|
296
|
+
style: {
|
|
297
|
+
backgroundColor: event.color || DEFAULT_EVENT_COLOR
|
|
298
|
+
},
|
|
299
|
+
title: event.title,
|
|
300
|
+
"data-testid": `event-${event.id}`,
|
|
301
|
+
onClick: (e) => handleEventClick(event, e),
|
|
302
|
+
children: event.title
|
|
303
|
+
},
|
|
304
|
+
event.id
|
|
305
|
+
)) })
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
`${cell.isCurrentMonth ? "cur" : "out"}-${key}`
|
|
309
|
+
);
|
|
310
|
+
})
|
|
311
|
+
] })
|
|
312
|
+
] });
|
|
313
|
+
};
|
|
314
|
+
export {
|
|
315
|
+
FullCalendar,
|
|
316
|
+
Timer
|
|
317
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orchestra-mcp/tracking",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analytics and tracking components for Orchestra MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/orchestra-mcp/tracking"
|
|
25
|
+
},
|
|
26
|
+
"funding": "https://github.com/sponsors/fadymondy",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18.0.0",
|
|
30
|
+
"react-dom": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.0.0",
|
|
34
|
+
"@types/react-dom": "^19.0.0",
|
|
35
|
+
"tsup": "^8.3.5",
|
|
36
|
+
"typescript": "^5.7.2"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
40
|
+
"prepublishOnly": "pnpm build"
|
|
41
|
+
}
|
|
42
|
+
}
|