omnira-ui 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/omnira-init.mjs +197 -16
- package/components/ui/ActivityGauge/ActivityGauge.module.css +109 -0
- package/components/ui/ActivityGauge/ActivityGauge.tsx +87 -0
- package/components/ui/ActivityGauge/index.ts +2 -0
- package/components/ui/Calendar/Calendar.module.css +492 -0
- package/components/ui/Calendar/Calendar.tsx +445 -0
- package/components/ui/Calendar/config.ts +130 -0
- package/components/ui/Calendar/index.ts +4 -0
- package/components/ui/CardHeader/CardHeader.module.css +79 -0
- package/components/ui/CardHeader/CardHeader.tsx +45 -0
- package/components/ui/CardHeader/index.ts +2 -0
- package/components/ui/EmptyState/EmptyState.module.css +65 -0
- package/components/ui/EmptyState/EmptyState.tsx +37 -0
- package/components/ui/EmptyState/index.ts +2 -0
- package/components/ui/Metric/Metric.module.css +140 -0
- package/components/ui/Metric/Metric.tsx +78 -0
- package/components/ui/Metric/index.ts +2 -0
- package/components/ui/PageHeader/PageHeader.module.css +128 -0
- package/components/ui/PageHeader/PageHeader.tsx +61 -0
- package/components/ui/PageHeader/index.ts +2 -0
- package/components/ui/Table/Table.module.css +444 -0
- package/components/ui/Table/Table.tsx +547 -0
- package/components/ui/Table/customers.json +74 -0
- package/components/ui/Table/index.ts +14 -0
- package/components/ui/Table/invoices.json +92 -0
- package/components/ui/Table/orders.json +108 -0
- package/components/ui/Table/team-members.json +130 -0
- package/components/ui/Table/uploaded-files.json +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useCallback } from "react";
|
|
4
|
+
import { ArrowLeft2, ArrowRight2 } from "iconsax-react";
|
|
5
|
+
import { cn } from "@/lib/cn";
|
|
6
|
+
import { Button } from "@/components/ui/Button";
|
|
7
|
+
import type { CalendarEvent } from "./config";
|
|
8
|
+
import styles from "./Calendar.module.css";
|
|
9
|
+
|
|
10
|
+
/* ── Types ── */
|
|
11
|
+
|
|
12
|
+
export type CalendarView = "month" | "week" | "day";
|
|
13
|
+
|
|
14
|
+
export interface CalendarProps {
|
|
15
|
+
events?: CalendarEvent[];
|
|
16
|
+
defaultView?: CalendarView;
|
|
17
|
+
defaultDate?: Date;
|
|
18
|
+
onViewChange?: (view: CalendarView) => void;
|
|
19
|
+
onDateChange?: (date: Date) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── Helpers ── */
|
|
24
|
+
|
|
25
|
+
const DAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
26
|
+
const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
|
27
|
+
|
|
28
|
+
function isSameDay(a: Date, b: Date) {
|
|
29
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isToday(d: Date) {
|
|
33
|
+
return isSameDay(d, new Date());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatTime(d: Date) {
|
|
37
|
+
return d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatHour(hour: number) {
|
|
41
|
+
if (hour === 0) return "12 AM";
|
|
42
|
+
if (hour < 12) return `${hour} AM`;
|
|
43
|
+
if (hour === 12) return "12 PM";
|
|
44
|
+
return `${hour - 12} PM`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMonthName(d: Date) {
|
|
48
|
+
return d.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getWeekLabel(d: Date) {
|
|
52
|
+
const start = getWeekStart(d);
|
|
53
|
+
const end = new Date(start);
|
|
54
|
+
end.setDate(end.getDate() + 6);
|
|
55
|
+
const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
|
|
56
|
+
if (start.getFullYear() !== end.getFullYear()) {
|
|
57
|
+
return `${start.toLocaleDateString(undefined, { ...opts, year: "numeric" })} – ${end.toLocaleDateString(undefined, { ...opts, year: "numeric" })}`;
|
|
58
|
+
}
|
|
59
|
+
return `${start.toLocaleDateString(undefined, opts)} – ${end.toLocaleDateString(undefined, { ...opts, year: "numeric" })}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getDayLabel(d: Date) {
|
|
63
|
+
return d.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getWeekStart(d: Date) {
|
|
67
|
+
const result = new Date(d);
|
|
68
|
+
result.setDate(result.getDate() - result.getDay());
|
|
69
|
+
result.setHours(0, 0, 0, 0);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getMonthDays(year: number, month: number) {
|
|
74
|
+
const firstDay = new Date(year, month, 1);
|
|
75
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
76
|
+
const days: { date: Date; isCurrentMonth: boolean }[] = [];
|
|
77
|
+
|
|
78
|
+
const startDow = firstDay.getDay();
|
|
79
|
+
for (let i = startDow - 1; i >= 0; i--) {
|
|
80
|
+
const d = new Date(year, month, -i);
|
|
81
|
+
days.push({ date: d, isCurrentMonth: false });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
85
|
+
days.push({ date: new Date(year, month, i), isCurrentMonth: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const remaining = 7 - (days.length % 7);
|
|
89
|
+
if (remaining < 7) {
|
|
90
|
+
for (let i = 1; i <= remaining; i++) {
|
|
91
|
+
days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return days;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getEventColorClass(color?: string) {
|
|
99
|
+
switch (color) {
|
|
100
|
+
case "lime": return styles.eventLime;
|
|
101
|
+
case "info": return styles.eventInfo;
|
|
102
|
+
case "warning": return styles.eventWarning;
|
|
103
|
+
case "error": return styles.eventError;
|
|
104
|
+
case "success": return styles.eventSuccess;
|
|
105
|
+
case "accent": return styles.eventAccent;
|
|
106
|
+
default: return styles.eventLime;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getEventsForDay(events: CalendarEvent[], date: Date) {
|
|
111
|
+
return events.filter((e) => isSameDay(e.start, date));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getNowOffset(): number {
|
|
115
|
+
const now = new Date();
|
|
116
|
+
return now.getHours() * 60 + now.getMinutes();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── Toolbar ── */
|
|
120
|
+
|
|
121
|
+
interface ToolbarProps {
|
|
122
|
+
title: string;
|
|
123
|
+
view: CalendarView;
|
|
124
|
+
onViewChange: (v: CalendarView) => void;
|
|
125
|
+
onPrev: () => void;
|
|
126
|
+
onNext: () => void;
|
|
127
|
+
onToday: () => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Toolbar({ title, view, onViewChange, onPrev, onNext, onToday }: ToolbarProps) {
|
|
131
|
+
return (
|
|
132
|
+
<div className={styles.toolbar}>
|
|
133
|
+
<div className={styles.toolbarLeft}>
|
|
134
|
+
<div className={styles.toolbarNav}>
|
|
135
|
+
<Button
|
|
136
|
+
variant="secondary"
|
|
137
|
+
size="sm"
|
|
138
|
+
iconOnly
|
|
139
|
+
icon={<ArrowLeft2 size={16} variant="Linear" color="currentColor" />}
|
|
140
|
+
onClick={onPrev}
|
|
141
|
+
aria-label="Previous"
|
|
142
|
+
/>
|
|
143
|
+
<Button
|
|
144
|
+
variant="secondary"
|
|
145
|
+
size="sm"
|
|
146
|
+
iconOnly
|
|
147
|
+
icon={<ArrowRight2 size={16} variant="Linear" color="currentColor" />}
|
|
148
|
+
onClick={onNext}
|
|
149
|
+
aria-label="Next"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<span className={styles.toolbarTitle}>{title}</span>
|
|
153
|
+
<Button variant="secondary" size="sm" onClick={onToday}>
|
|
154
|
+
Today
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
<div className={styles.viewTabs}>
|
|
158
|
+
{(["month", "week", "day"] as CalendarView[]).map((v) => (
|
|
159
|
+
<button
|
|
160
|
+
key={v}
|
|
161
|
+
type="button"
|
|
162
|
+
className={cn(styles.viewTab, view === v && styles.viewTabActive)}
|
|
163
|
+
onClick={() => onViewChange(v)}
|
|
164
|
+
>
|
|
165
|
+
{v.charAt(0).toUpperCase() + v.slice(1)}
|
|
166
|
+
</button>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── Month View ── */
|
|
174
|
+
|
|
175
|
+
interface MonthViewProps {
|
|
176
|
+
currentDate: Date;
|
|
177
|
+
events: CalendarEvent[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function MonthView({ currentDate, events }: MonthViewProps) {
|
|
181
|
+
const days = useMemo(
|
|
182
|
+
() => getMonthDays(currentDate.getFullYear(), currentDate.getMonth()),
|
|
183
|
+
[currentDate],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className={styles.monthGrid}>
|
|
188
|
+
{DAYS_SHORT.map((d) => (
|
|
189
|
+
<div key={d} className={styles.monthDayHeader}>{d}</div>
|
|
190
|
+
))}
|
|
191
|
+
{days.map(({ date, isCurrentMonth }, i) => {
|
|
192
|
+
const dayEvents = getEventsForDay(events, date);
|
|
193
|
+
const today = isToday(date);
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
key={i}
|
|
197
|
+
className={cn(
|
|
198
|
+
styles.monthCell,
|
|
199
|
+
!isCurrentMonth && styles.monthCellOutside,
|
|
200
|
+
today && styles.monthCellToday,
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
<span className={cn(styles.monthDayNumber, today && styles.monthDayNumberToday)}>
|
|
204
|
+
{date.getDate()}
|
|
205
|
+
</span>
|
|
206
|
+
{dayEvents.slice(0, 3).map((ev) => (
|
|
207
|
+
<div key={ev.id} className={cn(styles.monthEvent, getEventColorClass(ev.color))}>
|
|
208
|
+
{ev.title}
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
{dayEvents.length > 3 && (
|
|
212
|
+
<span className={styles.monthEventMore}>+{dayEvents.length - 3} more</span>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ── Week View ── */
|
|
222
|
+
|
|
223
|
+
interface WeekViewProps {
|
|
224
|
+
currentDate: Date;
|
|
225
|
+
events: CalendarEvent[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function WeekView({ currentDate, events }: WeekViewProps) {
|
|
229
|
+
const weekStart = useMemo(() => getWeekStart(currentDate), [currentDate]);
|
|
230
|
+
|
|
231
|
+
const weekDays = useMemo(() => {
|
|
232
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
233
|
+
const d = new Date(weekStart);
|
|
234
|
+
d.setDate(d.getDate() + i);
|
|
235
|
+
return d;
|
|
236
|
+
});
|
|
237
|
+
}, [weekStart]);
|
|
238
|
+
|
|
239
|
+
const nowMinutes = getNowOffset();
|
|
240
|
+
const showNow = weekDays.some((d) => isToday(d));
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className={styles.weekWrapper}>
|
|
244
|
+
<div className={styles.weekHeaderRow}>
|
|
245
|
+
<div className={styles.weekHeaderGutter} />
|
|
246
|
+
{weekDays.map((d, i) => {
|
|
247
|
+
const today = isToday(d);
|
|
248
|
+
return (
|
|
249
|
+
<div key={i} className={styles.weekHeaderCell}>
|
|
250
|
+
<div className={styles.weekHeaderDow}>{DAYS_SHORT[d.getDay()]}</div>
|
|
251
|
+
<div className={cn(styles.weekHeaderDate, today && styles.weekHeaderDateToday)}>
|
|
252
|
+
{d.getDate()}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</div>
|
|
258
|
+
<div className={styles.weekBody}>
|
|
259
|
+
<div className={styles.weekTimeGutter}>
|
|
260
|
+
{HOURS.map((h) => (
|
|
261
|
+
<div key={h} className={styles.weekTimeLabel}>{formatHour(h)}</div>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
{weekDays.map((day, di) => {
|
|
265
|
+
const dayEvents = getEventsForDay(events, day);
|
|
266
|
+
const today = isToday(day);
|
|
267
|
+
return (
|
|
268
|
+
<div key={di} className={styles.weekDayColumn}>
|
|
269
|
+
{HOURS.map((h) => (
|
|
270
|
+
<div key={h} className={styles.weekHourSlot} />
|
|
271
|
+
))}
|
|
272
|
+
{today && (
|
|
273
|
+
<div
|
|
274
|
+
className={styles.nowIndicator}
|
|
275
|
+
style={{ top: `${(nowMinutes / (24 * 60)) * 100}%` }}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
278
|
+
{dayEvents.map((ev) => {
|
|
279
|
+
const startMin = ev.start.getHours() * 60 + ev.start.getMinutes();
|
|
280
|
+
const endMin = ev.end.getHours() * 60 + ev.end.getMinutes();
|
|
281
|
+
const top = (startMin / (24 * 60)) * (24 * 60);
|
|
282
|
+
const height = ((endMin - startMin) / (24 * 60)) * (24 * 60);
|
|
283
|
+
return (
|
|
284
|
+
<div
|
|
285
|
+
key={ev.id}
|
|
286
|
+
className={cn(styles.weekEvent, getEventColorClass(ev.color))}
|
|
287
|
+
style={{
|
|
288
|
+
top: `${startMin}px`,
|
|
289
|
+
height: `${Math.max(endMin - startMin, 20)}px`,
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<div className={styles.weekEventTitle}>{ev.title}</div>
|
|
293
|
+
<div className={styles.weekEventTime}>
|
|
294
|
+
{formatTime(ev.start)} – {formatTime(ev.end)}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── Day View ── */
|
|
308
|
+
|
|
309
|
+
interface DayViewProps {
|
|
310
|
+
currentDate: Date;
|
|
311
|
+
events: CalendarEvent[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function DayView({ currentDate, events }: DayViewProps) {
|
|
315
|
+
const dayEvents = useMemo(() => getEventsForDay(events, currentDate), [events, currentDate]);
|
|
316
|
+
const today = isToday(currentDate);
|
|
317
|
+
const nowMinutes = getNowOffset();
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div className={styles.dayWrapper}>
|
|
321
|
+
<div className={styles.dayHeaderRow}>
|
|
322
|
+
<div className={styles.dayHeaderGutter} />
|
|
323
|
+
<div className={styles.dayHeaderCell}>
|
|
324
|
+
<div>
|
|
325
|
+
<div className={styles.dayHeaderDow}>{DAYS_SHORT[currentDate.getDay()]}</div>
|
|
326
|
+
<div className={cn(styles.dayHeaderDate, today && styles.dayHeaderDateToday)}>
|
|
327
|
+
{currentDate.getDate()}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div className={styles.dayBody}>
|
|
333
|
+
<div className={styles.dayTimeGutter}>
|
|
334
|
+
{HOURS.map((h) => (
|
|
335
|
+
<div key={h} className={styles.dayTimeLabel}>{formatHour(h)}</div>
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
<div className={styles.dayColumn}>
|
|
339
|
+
{HOURS.map((h) => (
|
|
340
|
+
<div key={h} className={styles.dayHourSlot} />
|
|
341
|
+
))}
|
|
342
|
+
{today && (
|
|
343
|
+
<div
|
|
344
|
+
className={styles.nowIndicator}
|
|
345
|
+
style={{ top: `${(nowMinutes / (24 * 60)) * 100}%` }}
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
{dayEvents.map((ev) => {
|
|
349
|
+
const startMin = ev.start.getHours() * 60 + ev.start.getMinutes();
|
|
350
|
+
const endMin = ev.end.getHours() * 60 + ev.end.getMinutes();
|
|
351
|
+
return (
|
|
352
|
+
<div
|
|
353
|
+
key={ev.id}
|
|
354
|
+
className={cn(styles.dayEvent, getEventColorClass(ev.color))}
|
|
355
|
+
style={{
|
|
356
|
+
top: `${startMin}px`,
|
|
357
|
+
height: `${Math.max(endMin - startMin, 30)}px`,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<div className={styles.dayEventTitle}>{ev.title}</div>
|
|
361
|
+
<div className={styles.dayEventTime}>
|
|
362
|
+
{formatTime(ev.start)} – {formatTime(ev.end)}
|
|
363
|
+
</div>
|
|
364
|
+
{ev.description && (endMin - startMin) >= 60 && (
|
|
365
|
+
<div className={styles.dayEventDescription}>{ev.description}</div>
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ── Calendar (main) ── */
|
|
377
|
+
|
|
378
|
+
export function Calendar({
|
|
379
|
+
events = [],
|
|
380
|
+
defaultView = "month",
|
|
381
|
+
defaultDate,
|
|
382
|
+
onViewChange,
|
|
383
|
+
onDateChange,
|
|
384
|
+
className,
|
|
385
|
+
}: CalendarProps) {
|
|
386
|
+
const [view, setView] = useState<CalendarView>(defaultView);
|
|
387
|
+
const [currentDate, setCurrentDate] = useState<Date>(defaultDate ?? new Date());
|
|
388
|
+
|
|
389
|
+
const handleViewChange = useCallback(
|
|
390
|
+
(v: CalendarView) => {
|
|
391
|
+
setView(v);
|
|
392
|
+
onViewChange?.(v);
|
|
393
|
+
},
|
|
394
|
+
[onViewChange],
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const handleDateChange = useCallback(
|
|
398
|
+
(d: Date) => {
|
|
399
|
+
setCurrentDate(d);
|
|
400
|
+
onDateChange?.(d);
|
|
401
|
+
},
|
|
402
|
+
[onDateChange],
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const navigate = useCallback(
|
|
406
|
+
(direction: -1 | 1) => {
|
|
407
|
+
const d = new Date(currentDate);
|
|
408
|
+
if (view === "month") {
|
|
409
|
+
d.setMonth(d.getMonth() + direction);
|
|
410
|
+
} else if (view === "week") {
|
|
411
|
+
d.setDate(d.getDate() + direction * 7);
|
|
412
|
+
} else {
|
|
413
|
+
d.setDate(d.getDate() + direction);
|
|
414
|
+
}
|
|
415
|
+
handleDateChange(d);
|
|
416
|
+
},
|
|
417
|
+
[currentDate, view, handleDateChange],
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const goToToday = useCallback(() => {
|
|
421
|
+
handleDateChange(new Date());
|
|
422
|
+
}, [handleDateChange]);
|
|
423
|
+
|
|
424
|
+
const title = useMemo(() => {
|
|
425
|
+
if (view === "month") return getMonthName(currentDate);
|
|
426
|
+
if (view === "week") return getWeekLabel(currentDate);
|
|
427
|
+
return getDayLabel(currentDate);
|
|
428
|
+
}, [view, currentDate]);
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<div className={cn(styles.calendarRoot, className)}>
|
|
432
|
+
<Toolbar
|
|
433
|
+
title={title}
|
|
434
|
+
view={view}
|
|
435
|
+
onViewChange={handleViewChange}
|
|
436
|
+
onPrev={() => navigate(-1)}
|
|
437
|
+
onNext={() => navigate(1)}
|
|
438
|
+
onToday={goToToday}
|
|
439
|
+
/>
|
|
440
|
+
{view === "month" && <MonthView currentDate={currentDate} events={events} />}
|
|
441
|
+
{view === "week" && <WeekView currentDate={currentDate} events={events} />}
|
|
442
|
+
{view === "day" && <DayView currentDate={currentDate} events={events} />}
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export interface CalendarEvent {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
start: Date;
|
|
5
|
+
end: Date;
|
|
6
|
+
color?: "lime" | "info" | "warning" | "error" | "success" | "accent";
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getRelativeDate(dayOffset: number, hour: number, minute = 0): Date {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
d.setDate(d.getDate() + dayOffset);
|
|
13
|
+
d.setHours(hour, minute, 0, 0);
|
|
14
|
+
return d;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const events: CalendarEvent[] = [
|
|
18
|
+
{
|
|
19
|
+
id: "1",
|
|
20
|
+
title: "Design Review",
|
|
21
|
+
start: getRelativeDate(0, 9, 0),
|
|
22
|
+
end: getRelativeDate(0, 10, 0),
|
|
23
|
+
color: "lime",
|
|
24
|
+
description: "Review new dashboard designs with the team",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "2",
|
|
28
|
+
title: "Sprint Planning",
|
|
29
|
+
start: getRelativeDate(0, 13, 0),
|
|
30
|
+
end: getRelativeDate(0, 14, 30),
|
|
31
|
+
color: "info",
|
|
32
|
+
description: "Plan next sprint tasks and priorities",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "3",
|
|
36
|
+
title: "Team Standup",
|
|
37
|
+
start: getRelativeDate(1, 9, 30),
|
|
38
|
+
end: getRelativeDate(1, 10, 0),
|
|
39
|
+
color: "success",
|
|
40
|
+
description: "Daily standup meeting",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "4",
|
|
44
|
+
title: "Client Call",
|
|
45
|
+
start: getRelativeDate(1, 14, 0),
|
|
46
|
+
end: getRelativeDate(1, 15, 0),
|
|
47
|
+
color: "warning",
|
|
48
|
+
description: "Quarterly review with client stakeholders",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "5",
|
|
52
|
+
title: "Code Review",
|
|
53
|
+
start: getRelativeDate(2, 11, 0),
|
|
54
|
+
end: getRelativeDate(2, 12, 0),
|
|
55
|
+
color: "accent",
|
|
56
|
+
description: "Review pull requests for the new feature branch",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "6",
|
|
60
|
+
title: "Product Demo",
|
|
61
|
+
start: getRelativeDate(2, 15, 0),
|
|
62
|
+
end: getRelativeDate(2, 16, 30),
|
|
63
|
+
color: "error",
|
|
64
|
+
description: "Demo the latest release to stakeholders",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "7",
|
|
68
|
+
title: "1:1 with Manager",
|
|
69
|
+
start: getRelativeDate(3, 10, 0),
|
|
70
|
+
end: getRelativeDate(3, 10, 30),
|
|
71
|
+
color: "info",
|
|
72
|
+
description: "Weekly one-on-one meeting",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "8",
|
|
76
|
+
title: "Workshop: API Design",
|
|
77
|
+
start: getRelativeDate(3, 14, 0),
|
|
78
|
+
end: getRelativeDate(3, 16, 0),
|
|
79
|
+
color: "lime",
|
|
80
|
+
description: "Internal workshop on REST API best practices",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "9",
|
|
84
|
+
title: "Lunch & Learn",
|
|
85
|
+
start: getRelativeDate(4, 12, 0),
|
|
86
|
+
end: getRelativeDate(4, 13, 0),
|
|
87
|
+
color: "success",
|
|
88
|
+
description: "Presentation on new testing strategies",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "10",
|
|
92
|
+
title: "Release Planning",
|
|
93
|
+
start: getRelativeDate(5, 10, 0),
|
|
94
|
+
end: getRelativeDate(5, 11, 30),
|
|
95
|
+
color: "warning",
|
|
96
|
+
description: "Plan the next product release cycle",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "11",
|
|
100
|
+
title: "Design System Sync",
|
|
101
|
+
start: getRelativeDate(-1, 9, 0),
|
|
102
|
+
end: getRelativeDate(-1, 10, 0),
|
|
103
|
+
color: "accent",
|
|
104
|
+
description: "Sync on component library updates",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "12",
|
|
108
|
+
title: "Retrospective",
|
|
109
|
+
start: getRelativeDate(-2, 15, 0),
|
|
110
|
+
end: getRelativeDate(-2, 16, 0),
|
|
111
|
+
color: "info",
|
|
112
|
+
description: "Sprint retrospective and action items",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "13",
|
|
116
|
+
title: "Marketing Sync",
|
|
117
|
+
start: getRelativeDate(6, 11, 0),
|
|
118
|
+
end: getRelativeDate(6, 12, 0),
|
|
119
|
+
color: "error",
|
|
120
|
+
description: "Align on upcoming campaign launch",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "14",
|
|
124
|
+
title: "Infrastructure Review",
|
|
125
|
+
start: getRelativeDate(0, 16, 0),
|
|
126
|
+
end: getRelativeDate(0, 17, 0),
|
|
127
|
+
color: "warning",
|
|
128
|
+
description: "Review cloud infrastructure and costs",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
Card Header — Glassmorphism Styles
|
|
3
|
+
============================================ */
|
|
4
|
+
|
|
5
|
+
.cardHeader {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: flex-start;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
gap: 16px;
|
|
10
|
+
padding: 20px 24px;
|
|
11
|
+
border-radius: var(--radius-lg);
|
|
12
|
+
background: var(--color-bg-card);
|
|
13
|
+
border: 1px solid var(--color-border-standard);
|
|
14
|
+
box-shadow: var(--shadow-card);
|
|
15
|
+
backdrop-filter: var(--blur-standard);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cardHeaderDivider {
|
|
19
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
20
|
+
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── Left content ── */
|
|
24
|
+
|
|
25
|
+
.content {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
gap: 16px;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
flex: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.iconWrapper {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
width: 44px;
|
|
38
|
+
height: 44px;
|
|
39
|
+
border-radius: var(--radius-md);
|
|
40
|
+
background: var(--color-bg-elevated);
|
|
41
|
+
border: 1px solid var(--color-border-subtle);
|
|
42
|
+
color: var(--color-text-secondary);
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.textContent {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.titleRow {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 8px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.title {
|
|
59
|
+
font-family: var(--font-display);
|
|
60
|
+
font-size: 16px;
|
|
61
|
+
font-weight: 700;
|
|
62
|
+
color: var(--color-text-primary);
|
|
63
|
+
line-height: 1.3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.description {
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
color: var(--color-text-tertiary);
|
|
69
|
+
line-height: 1.4;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Right actions ── */
|
|
73
|
+
|
|
74
|
+
.actions {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cn } from "@/lib/cn";
|
|
2
|
+
import styles from "./CardHeader.module.css";
|
|
3
|
+
|
|
4
|
+
/* ── Types ── */
|
|
5
|
+
|
|
6
|
+
export interface CardHeaderProps {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
avatar?: React.ReactNode;
|
|
11
|
+
badge?: React.ReactNode;
|
|
12
|
+
actions?: React.ReactNode;
|
|
13
|
+
divider?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ── Component ── */
|
|
18
|
+
|
|
19
|
+
export function CardHeader({
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
icon,
|
|
23
|
+
avatar,
|
|
24
|
+
badge,
|
|
25
|
+
actions,
|
|
26
|
+
divider = false,
|
|
27
|
+
className,
|
|
28
|
+
}: CardHeaderProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn(styles.cardHeader, divider && styles.cardHeaderDivider, className)}>
|
|
31
|
+
<div className={styles.content}>
|
|
32
|
+
{avatar}
|
|
33
|
+
{icon && <div className={styles.iconWrapper}>{icon}</div>}
|
|
34
|
+
<div className={styles.textContent}>
|
|
35
|
+
<div className={styles.titleRow}>
|
|
36
|
+
<h3 className={styles.title}>{title}</h3>
|
|
37
|
+
{badge}
|
|
38
|
+
</div>
|
|
39
|
+
{description && <p className={styles.description}>{description}</p>}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
{actions && <div className={styles.actions}>{actions}</div>}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|