@moontra/moonui-pro 2.5.13 → 2.6.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/dist/index.d.ts +2061 -0
- package/dist/index.mjs +4259 -1229
- package/package.json +1 -1
- package/src/components/dashboard/demo.tsx +116 -2
- package/src/components/dashboard/index.tsx +318 -22
- package/src/components/dashboard/time-range-picker.tsx +317 -250
- package/src/components/sidebar/index.tsx +161 -2
- package/src/components/ui/calendar.tsx +376 -54
- package/src/components/ui/hover-card.tsx +29 -0
- package/src/components/ui/index.ts +4 -0
- package/src/styles/calendar.css +35 -0
- package/src/styles/index.css +1 -0
|
@@ -1,269 +1,336 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import React from
|
|
4
|
-
import { motion } from
|
|
5
|
-
import { Button } from
|
|
6
|
-
import { Calendar } from
|
|
7
|
-
import { Popover, PopoverContent, PopoverTrigger } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { Button } from "../ui/button";
|
|
6
|
+
import { Calendar } from "../ui/calendar";
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
8
|
+
import { cn } from "../../lib/utils";
|
|
9
|
+
import { CalendarIcon, ChevronDown } from "lucide-react";
|
|
10
|
+
import {
|
|
11
|
+
format,
|
|
12
|
+
startOfDay,
|
|
13
|
+
endOfDay,
|
|
14
|
+
subDays,
|
|
15
|
+
startOfWeek,
|
|
16
|
+
endOfWeek,
|
|
17
|
+
startOfMonth,
|
|
18
|
+
endOfMonth,
|
|
19
|
+
} from "date-fns";
|
|
20
|
+
import { TimeRange } from "./types";
|
|
13
21
|
|
|
14
22
|
interface TimeRangePickerProps {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
value?: TimeRange;
|
|
24
|
+
onChange?: (range: TimeRange) => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
showPresets?: boolean;
|
|
27
|
+
showComparison?: boolean;
|
|
28
|
+
glassmorphism?: boolean;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
const PRESET_RANGES = [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
]
|
|
32
|
+
{
|
|
33
|
+
label: "Today",
|
|
34
|
+
value: "today",
|
|
35
|
+
getRange: () => ({
|
|
36
|
+
start: startOfDay(new Date()),
|
|
37
|
+
end: endOfDay(new Date()),
|
|
38
|
+
label: "Today",
|
|
39
|
+
preset: "today" as const,
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Yesterday",
|
|
44
|
+
value: "yesterday",
|
|
45
|
+
getRange: () => ({
|
|
46
|
+
start: startOfDay(subDays(new Date(), 1)),
|
|
47
|
+
end: endOfDay(subDays(new Date(), 1)),
|
|
48
|
+
label: "Yesterday",
|
|
49
|
+
preset: "yesterday" as const,
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Last 7 days",
|
|
54
|
+
value: "last7days",
|
|
55
|
+
getRange: () => ({
|
|
56
|
+
start: startOfDay(subDays(new Date(), 6)),
|
|
57
|
+
end: endOfDay(new Date()),
|
|
58
|
+
label: "Last 7 days",
|
|
59
|
+
preset: "last7days" as const,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "Last 30 days",
|
|
64
|
+
value: "last30days",
|
|
65
|
+
getRange: () => ({
|
|
66
|
+
start: startOfDay(subDays(new Date(), 29)),
|
|
67
|
+
end: endOfDay(new Date()),
|
|
68
|
+
label: "Last 30 days",
|
|
69
|
+
preset: "last30days" as const,
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: "This week",
|
|
74
|
+
value: "thisWeek",
|
|
75
|
+
getRange: () => ({
|
|
76
|
+
start: startOfWeek(new Date()),
|
|
77
|
+
end: endOfWeek(new Date()),
|
|
78
|
+
label: "This week",
|
|
79
|
+
preset: "custom" as const,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "This month",
|
|
84
|
+
value: "thisMonth",
|
|
85
|
+
getRange: () => ({
|
|
86
|
+
start: startOfMonth(new Date()),
|
|
87
|
+
end: endOfMonth(new Date()),
|
|
88
|
+
label: "This month",
|
|
89
|
+
preset: "thisMonth" as const,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "Last month",
|
|
94
|
+
value: "lastMonth",
|
|
95
|
+
getRange: () => {
|
|
96
|
+
const lastMonth = subDays(startOfMonth(new Date()), 1);
|
|
97
|
+
return {
|
|
98
|
+
start: startOfMonth(lastMonth),
|
|
99
|
+
end: endOfMonth(lastMonth),
|
|
100
|
+
label: "Last month",
|
|
101
|
+
preset: "lastMonth" as const,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
98
106
|
|
|
99
107
|
export function TimeRangePicker({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
value,
|
|
109
|
+
onChange,
|
|
110
|
+
className,
|
|
111
|
+
showPresets = true,
|
|
112
|
+
showComparison = false,
|
|
113
|
+
glassmorphism = false,
|
|
106
114
|
}: TimeRangePickerProps) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
116
|
+
const [customRange, setCustomRange] = React.useState<{
|
|
117
|
+
from?: Date;
|
|
118
|
+
to?: Date;
|
|
119
|
+
}>({
|
|
120
|
+
from: undefined,
|
|
121
|
+
to: undefined,
|
|
122
|
+
});
|
|
123
|
+
const [comparisonEnabled, setComparisonEnabled] = React.useState(false);
|
|
110
124
|
|
|
111
|
-
|
|
125
|
+
const currentRange = value || PRESET_RANGES[2].getRange(); // Default: Last 7 days
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
// Format tarih aralığı
|
|
128
|
+
const formatRange = (range: TimeRange) => {
|
|
129
|
+
if (range.preset && range.preset !== "custom") {
|
|
130
|
+
return range.label;
|
|
131
|
+
}
|
|
132
|
+
return `${format(range.start, "MMM d")} - ${format(range.end, "MMM d, yyyy")}`;
|
|
133
|
+
};
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
// Preset seçimi
|
|
136
|
+
const handlePresetSelect = (preset: (typeof PRESET_RANGES)[0]) => {
|
|
137
|
+
const range = preset.getRange();
|
|
138
|
+
onChange?.(range);
|
|
139
|
+
setIsOpen(false);
|
|
140
|
+
};
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// Custom tarih seçimi
|
|
143
|
+
const handleCustomRangeSelect = () => {
|
|
144
|
+
if (customRange.from && customRange.to) {
|
|
145
|
+
onChange?.({
|
|
146
|
+
start: startOfDay(customRange.from),
|
|
147
|
+
end: endOfDay(customRange.to),
|
|
148
|
+
label: `${format(customRange.from, "MMM d")} - ${format(customRange.to, "MMM d, yyyy")}`,
|
|
149
|
+
preset: "custom",
|
|
150
|
+
});
|
|
151
|
+
setIsOpen(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
side="bottom"
|
|
165
|
-
sideOffset={4}
|
|
166
|
-
>
|
|
167
|
-
<motion.div
|
|
168
|
-
initial={{ opacity: 0, y: -10 }}
|
|
169
|
-
animate={{ opacity: 1, y: 0 }}
|
|
170
|
-
transition={{ duration: 0.2 }}
|
|
171
|
-
>
|
|
172
|
-
<div className="flex">
|
|
173
|
-
{/* Preset'ler */}
|
|
174
|
-
{showPresets && (
|
|
175
|
-
<div className="border-r p-3">
|
|
176
|
-
<div className="flex items-center gap-2 px-2 pb-2">
|
|
177
|
-
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
178
|
-
<h4 className="text-sm font-medium">Quick Select</h4>
|
|
179
|
-
</div>
|
|
180
|
-
<div className="space-y-1">
|
|
181
|
-
{PRESET_RANGES.map((preset) => (
|
|
182
|
-
<motion.button
|
|
183
|
-
key={preset.value}
|
|
184
|
-
whileHover={{ x: 2 }}
|
|
185
|
-
whileTap={{ scale: 0.98 }}
|
|
186
|
-
onClick={() => handlePresetSelect(preset)}
|
|
187
|
-
className={cn(
|
|
188
|
-
"w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors",
|
|
189
|
-
"hover:bg-muted",
|
|
190
|
-
currentRange.preset === preset.value && "bg-primary text-primary-foreground"
|
|
191
|
-
)}
|
|
192
|
-
>
|
|
193
|
-
{preset.label}
|
|
194
|
-
</motion.button>
|
|
195
|
-
))}
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
{/* Karşılaştırma */}
|
|
199
|
-
{showComparison && (
|
|
200
|
-
<div className="mt-4 pt-4 border-t">
|
|
201
|
-
<label className="flex items-center gap-2 px-2 cursor-pointer">
|
|
202
|
-
<input
|
|
203
|
-
type="checkbox"
|
|
204
|
-
checked={comparisonEnabled}
|
|
205
|
-
onChange={(e) => setComparisonEnabled(e.target.checked)}
|
|
206
|
-
className="rounded"
|
|
207
|
-
/>
|
|
208
|
-
<span className="text-sm">Compare to previous period</span>
|
|
209
|
-
</label>
|
|
210
|
-
</div>
|
|
155
|
+
return (
|
|
156
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
157
|
+
<PopoverTrigger asChild>
|
|
158
|
+
<Button
|
|
159
|
+
variant="outline"
|
|
160
|
+
className={cn(
|
|
161
|
+
"justify-start text-left font-normal",
|
|
162
|
+
glassmorphism &&
|
|
163
|
+
"bg-background/60 backdrop-blur-sm border-white/10",
|
|
164
|
+
!value && "text-muted-foreground",
|
|
165
|
+
className
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
169
|
+
{formatRange(currentRange)}
|
|
170
|
+
<ChevronDown className="ml-auto h-4 w-4 opacity-50" />
|
|
171
|
+
</Button>
|
|
172
|
+
</PopoverTrigger>
|
|
173
|
+
<PopoverContent
|
|
174
|
+
className={cn(
|
|
175
|
+
"time-range-popover w-auto max-w-[95vw] sm:max-w-4xl p-0 overflow-hidden",
|
|
176
|
+
glassmorphism &&
|
|
177
|
+
"bg-background/95 backdrop-blur-md border-white/10"
|
|
211
178
|
)}
|
|
212
|
-
|
|
213
|
-
|
|
179
|
+
align="end"
|
|
180
|
+
side="bottom"
|
|
181
|
+
sideOffset={4}
|
|
182
|
+
collisionPadding={16}
|
|
183
|
+
>
|
|
184
|
+
<motion.div
|
|
185
|
+
initial={{ opacity: 0, y: -10 }}
|
|
186
|
+
animate={{ opacity: 1, y: 0 }}
|
|
187
|
+
transition={{ duration: 0.2 }}
|
|
188
|
+
className="min-w-0"
|
|
189
|
+
>
|
|
190
|
+
{/* Mobile: Stacked Layout */}
|
|
191
|
+
<div className="sm:hidden">
|
|
192
|
+
{/* Custom tarih seçici */}
|
|
193
|
+
<div className="p-3">
|
|
194
|
+
{/* Karşılaştırma */}
|
|
195
|
+
{showComparison && (
|
|
196
|
+
<div className="mt-3 pt-3 border-t">
|
|
197
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
198
|
+
<input
|
|
199
|
+
type="checkbox"
|
|
200
|
+
checked={comparisonEnabled}
|
|
201
|
+
onChange={(e) =>
|
|
202
|
+
setComparisonEnabled(
|
|
203
|
+
e.target.checked
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
className="rounded w-3 h-3"
|
|
207
|
+
/>
|
|
208
|
+
<span className="text-xs">
|
|
209
|
+
Compare to previous
|
|
210
|
+
</span>
|
|
211
|
+
</label>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Desktop: Side by Side Layout */}
|
|
218
|
+
<div className="hidden sm:flex">
|
|
219
|
+
{/* Preset'ler */}
|
|
220
|
+
{showPresets && (
|
|
221
|
+
<div className="preset-column border-r p-3 min-w-[140px]">
|
|
222
|
+
<div className="grid grid-cols-1 gap-1">
|
|
223
|
+
{PRESET_RANGES.map((preset) => (
|
|
224
|
+
<motion.button
|
|
225
|
+
key={preset.value}
|
|
226
|
+
whileHover={{ x: 2 }}
|
|
227
|
+
whileTap={{ scale: 0.98 }}
|
|
228
|
+
onClick={() =>
|
|
229
|
+
handlePresetSelect(preset)
|
|
230
|
+
}
|
|
231
|
+
className={cn(
|
|
232
|
+
"w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors whitespace-nowrap",
|
|
233
|
+
"hover:bg-muted",
|
|
234
|
+
currentRange.preset ===
|
|
235
|
+
preset.value &&
|
|
236
|
+
"bg-primary text-primary-foreground"
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
{preset.label}
|
|
240
|
+
</motion.button>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* Karşılaştırma */}
|
|
245
|
+
{showComparison && (
|
|
246
|
+
<div className="mt-4 pt-4 border-t">
|
|
247
|
+
<label className="flex items-center gap-2 px-2 cursor-pointer">
|
|
248
|
+
<input
|
|
249
|
+
type="checkbox"
|
|
250
|
+
checked={comparisonEnabled}
|
|
251
|
+
onChange={(e) =>
|
|
252
|
+
setComparisonEnabled(
|
|
253
|
+
e.target.checked
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
className="rounded w-4 h-4"
|
|
257
|
+
/>
|
|
258
|
+
<span className="text-sm">
|
|
259
|
+
Compare to previous period
|
|
260
|
+
</span>
|
|
261
|
+
</label>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
214
266
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
{/* Custom tarih seçici */}
|
|
268
|
+
<div className="p-3 flex-1">
|
|
269
|
+
<div className="overflow-x-auto">
|
|
270
|
+
<Calendar
|
|
271
|
+
mode="range"
|
|
272
|
+
selected={{
|
|
273
|
+
from: customRange.from,
|
|
274
|
+
to: customRange.to,
|
|
275
|
+
}}
|
|
276
|
+
onSelect={(range: any) =>
|
|
277
|
+
setCustomRange(
|
|
278
|
+
range || {
|
|
279
|
+
from: undefined,
|
|
280
|
+
to: undefined,
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
numberOfMonths={2}
|
|
285
|
+
showOutsideDays={true}
|
|
286
|
+
className="rounded-md"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
232
289
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
290
|
+
<div className="flex items-center justify-between pt-3 px-2">
|
|
291
|
+
<div className="text-sm text-muted-foreground">
|
|
292
|
+
{customRange.from && customRange.to && (
|
|
293
|
+
<span>
|
|
294
|
+
{Math.ceil(
|
|
295
|
+
(customRange.to.getTime() -
|
|
296
|
+
customRange.from.getTime()) /
|
|
297
|
+
(1000 * 60 * 60 * 24)
|
|
298
|
+
)}{" "}
|
|
299
|
+
days selected
|
|
300
|
+
</span>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
<div className="flex gap-2">
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="sm"
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setCustomRange({
|
|
309
|
+
from: undefined,
|
|
310
|
+
to: undefined,
|
|
311
|
+
});
|
|
312
|
+
setIsOpen(false);
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
Cancel
|
|
316
|
+
</Button>
|
|
317
|
+
<Button
|
|
318
|
+
size="sm"
|
|
319
|
+
onClick={handleCustomRangeSelect}
|
|
320
|
+
disabled={
|
|
321
|
+
!customRange.from || !customRange.to
|
|
322
|
+
}
|
|
323
|
+
>
|
|
324
|
+
Apply
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</motion.div>
|
|
331
|
+
</PopoverContent>
|
|
332
|
+
</Popover>
|
|
333
|
+
);
|
|
267
334
|
}
|
|
268
335
|
|
|
269
|
-
export default TimeRangePicker
|
|
336
|
+
export default TimeRangePicker;
|