@thangnqdev/ui 0.1.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/README.md +196 -0
- package/dist/Label-DWOlgSNW.d.cts +14 -0
- package/dist/Label-DWOlgSNW.d.ts +14 -0
- package/dist/bento/index.cjs +140 -0
- package/dist/bento/index.cjs.map +1 -0
- package/dist/bento/index.d.cts +4 -0
- package/dist/bento/index.d.ts +4 -0
- package/dist/bento/index.js +4 -0
- package/dist/bento/index.js.map +1 -0
- package/dist/bento/styles.css +46 -0
- package/dist/buttons/index.cjs +365 -0
- package/dist/buttons/index.cjs.map +1 -0
- package/dist/buttons/index.d.cts +4 -0
- package/dist/buttons/index.d.ts +4 -0
- package/dist/buttons/index.js +5 -0
- package/dist/buttons/index.js.map +1 -0
- package/dist/buttons/styles.css +6 -0
- package/dist/chunk-2OXZK6IP.js +514 -0
- package/dist/chunk-2OXZK6IP.js.map +1 -0
- package/dist/chunk-4S6L5V7X.js +15 -0
- package/dist/chunk-4S6L5V7X.js.map +1 -0
- package/dist/chunk-5FZ6ZFII.js +728 -0
- package/dist/chunk-5FZ6ZFII.js.map +1 -0
- package/dist/chunk-66VWUJEI.js +86 -0
- package/dist/chunk-66VWUJEI.js.map +1 -0
- package/dist/chunk-A7P3O5Z5.js +163 -0
- package/dist/chunk-A7P3O5Z5.js.map +1 -0
- package/dist/chunk-ATYGDTMK.js +257 -0
- package/dist/chunk-ATYGDTMK.js.map +1 -0
- package/dist/chunk-C4SUXMI3.js +138 -0
- package/dist/chunk-C4SUXMI3.js.map +1 -0
- package/dist/chunk-EU2LPUYP.js +25 -0
- package/dist/chunk-EU2LPUYP.js.map +1 -0
- package/dist/chunk-F5Q7BTMH.js +1991 -0
- package/dist/chunk-F5Q7BTMH.js.map +1 -0
- package/dist/chunk-GXI2H5XA.js +46 -0
- package/dist/chunk-GXI2H5XA.js.map +1 -0
- package/dist/chunk-H3LCNPMC.js +101 -0
- package/dist/chunk-H3LCNPMC.js.map +1 -0
- package/dist/chunk-IABPUCAQ.js +19 -0
- package/dist/chunk-IABPUCAQ.js.map +1 -0
- package/dist/chunk-KJRBRTQE.js +101 -0
- package/dist/chunk-KJRBRTQE.js.map +1 -0
- package/dist/chunk-LBUBXUYQ.js +40 -0
- package/dist/chunk-LBUBXUYQ.js.map +1 -0
- package/dist/chunk-LWFRUJRU.js +537 -0
- package/dist/chunk-LWFRUJRU.js.map +1 -0
- package/dist/chunk-NFVONJUK.js +882 -0
- package/dist/chunk-NFVONJUK.js.map +1 -0
- package/dist/chunk-PF4ZHGVW.js +284 -0
- package/dist/chunk-PF4ZHGVW.js.map +1 -0
- package/dist/chunk-WAFHC4DQ.js +366 -0
- package/dist/chunk-WAFHC4DQ.js.map +1 -0
- package/dist/chunk-X76KBH4Y.js +71 -0
- package/dist/chunk-X76KBH4Y.js.map +1 -0
- package/dist/core/index.cjs +103 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/styles.css +128 -0
- package/dist/data-display/index.cjs +1023 -0
- package/dist/data-display/index.cjs.map +1 -0
- package/dist/data-display/index.d.cts +4 -0
- package/dist/data-display/index.d.ts +4 -0
- package/dist/data-display/index.js +6 -0
- package/dist/data-display/index.js.map +1 -0
- package/dist/data-display/styles.css +4 -0
- package/dist/feedback/index.cjs +368 -0
- package/dist/feedback/index.cjs.map +1 -0
- package/dist/feedback/index.d.cts +4 -0
- package/dist/feedback/index.d.ts +4 -0
- package/dist/feedback/index.js +4 -0
- package/dist/feedback/index.js.map +1 -0
- package/dist/feedback/styles.css +4 -0
- package/dist/forms/index.cjs +2287 -0
- package/dist/forms/index.cjs.map +1 -0
- package/dist/forms/index.d.cts +5 -0
- package/dist/forms/index.d.ts +5 -0
- package/dist/forms/index.js +10 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/styles.css +6 -0
- package/dist/glass/index.cjs +165 -0
- package/dist/glass/index.cjs.map +1 -0
- package/dist/glass/index.d.cts +4 -0
- package/dist/glass/index.d.ts +4 -0
- package/dist/glass/index.js +4 -0
- package/dist/glass/index.js.map +1 -0
- package/dist/glass/styles.css +81 -0
- package/dist/index-BTvkbCIj.d.cts +146 -0
- package/dist/index-BTvkbCIj.d.ts +146 -0
- package/dist/index-BkRqGNG1.d.cts +201 -0
- package/dist/index-BkRqGNG1.d.ts +201 -0
- package/dist/index-BlPcGeW-.d.cts +45 -0
- package/dist/index-BlPcGeW-.d.ts +45 -0
- package/dist/index-BzOCqvC6.d.cts +32 -0
- package/dist/index-BzOCqvC6.d.ts +32 -0
- package/dist/index-CCeWYAvn.d.cts +83 -0
- package/dist/index-COQ7uwt9.d.cts +44 -0
- package/dist/index-COQ7uwt9.d.ts +44 -0
- package/dist/index-CenuF_P2.d.cts +221 -0
- package/dist/index-CenuF_P2.d.ts +221 -0
- package/dist/index-Cle6QzQl.d.ts +371 -0
- package/dist/index-D6fE7w-b.d.cts +90 -0
- package/dist/index-D6fE7w-b.d.ts +90 -0
- package/dist/index-DR8cfJUG.d.ts +83 -0
- package/dist/index-DzcWh1Hs.d.cts +131 -0
- package/dist/index-DzcWh1Hs.d.ts +131 -0
- package/dist/index-TyK-Ldpy.d.cts +371 -0
- package/dist/index-ugj5s6a3.d.cts +95 -0
- package/dist/index-ugj5s6a3.d.ts +95 -0
- package/dist/index.cjs +6400 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/index.cjs +516 -0
- package/dist/layout/index.cjs.map +1 -0
- package/dist/layout/index.d.cts +4 -0
- package/dist/layout/index.d.ts +4 -0
- package/dist/layout/index.js +4 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/styles.css +8 -0
- package/dist/navigation/index.cjs +673 -0
- package/dist/navigation/index.cjs.map +1 -0
- package/dist/navigation/index.d.cts +2 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +8 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/styles.css +4 -0
- package/dist/overlays/index.cjs +954 -0
- package/dist/overlays/index.cjs.map +1 -0
- package/dist/overlays/index.d.cts +4 -0
- package/dist/overlays/index.d.ts +4 -0
- package/dist/overlays/index.js +9 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/overlays/styles.css +8 -0
- package/dist/styles.css +333 -0
- package/dist/typography/index.cjs +311 -0
- package/dist/typography/index.cjs.map +1 -0
- package/dist/typography/index.d.cts +5 -0
- package/dist/typography/index.d.ts +5 -0
- package/dist/typography/index.js +6 -0
- package/dist/typography/index.js.map +1 -0
- package/dist/typography/styles.css +5 -0
- package/llms.txt +113 -0
- package/package.json +116 -0
- package/src/bento/BentoCard.tsx +140 -0
- package/src/bento/BentoGrid.tsx +50 -0
- package/src/bento/index.ts +2 -0
- package/src/bento/tokens.css +46 -0
- package/src/buttons/Button.tsx +104 -0
- package/src/buttons/ButtonGroup.tsx +43 -0
- package/src/buttons/IconButton.tsx +79 -0
- package/src/buttons/SplitButton.tsx +86 -0
- package/src/buttons/ToggleButton.tsx +157 -0
- package/src/buttons/index.ts +5 -0
- package/src/buttons/tokens.css +6 -0
- package/src/core/ThemeProvider.tsx +123 -0
- package/src/core/index.ts +2 -0
- package/src/core/tokens.css +128 -0
- package/src/data-display/Accordion.tsx +147 -0
- package/src/data-display/Avatar.tsx +134 -0
- package/src/data-display/Card.tsx +112 -0
- package/src/data-display/Carousel.tsx +130 -0
- package/src/data-display/Chip.tsx +74 -0
- package/src/data-display/DataTablePro.tsx +356 -0
- package/src/data-display/StatCard.tsx +60 -0
- package/src/data-display/Table.tsx +74 -0
- package/src/data-display/Timeline.tsx +62 -0
- package/src/data-display/TreeView.tsx +140 -0
- package/src/data-display/index.ts +10 -0
- package/src/data-display/tokens.css +4 -0
- package/src/feedback/Alert.tsx +80 -0
- package/src/feedback/EmptyState.tsx +45 -0
- package/src/feedback/ProgressBar.tsx +174 -0
- package/src/feedback/Skeleton.tsx +66 -0
- package/src/feedback/Spinner.tsx +65 -0
- package/src/feedback/index.ts +5 -0
- package/src/feedback/tokens.css +4 -0
- package/src/forms/Checkbox.tsx +69 -0
- package/src/forms/CheckboxGroup.tsx +29 -0
- package/src/forms/Combobox.tsx +368 -0
- package/src/forms/DatePicker.tsx +480 -0
- package/src/forms/FileUpload.tsx +222 -0
- package/src/forms/FormField.tsx +115 -0
- package/src/forms/Input.tsx +151 -0
- package/src/forms/MaskedInput.tsx +97 -0
- package/src/forms/NumberInput.tsx +248 -0
- package/src/forms/PasswordInput.tsx +35 -0
- package/src/forms/PinInput.tsx +125 -0
- package/src/forms/Radio.tsx +142 -0
- package/src/forms/RangeSlider.tsx +111 -0
- package/src/forms/SearchInput.tsx +34 -0
- package/src/forms/Select.tsx +103 -0
- package/src/forms/Slider.tsx +85 -0
- package/src/forms/Switch.tsx +95 -0
- package/src/forms/Textarea.tsx +114 -0
- package/src/forms/index.ts +18 -0
- package/src/forms/tokens.css +6 -0
- package/src/glass/GlassBadge.tsx +61 -0
- package/src/glass/GlassButton.tsx +59 -0
- package/src/glass/GlassCard.tsx +71 -0
- package/src/glass/index.ts +3 -0
- package/src/glass/tokens.css +81 -0
- package/src/index.ts +26 -0
- package/src/layout/AspectRatio.tsx +39 -0
- package/src/layout/Container.tsx +53 -0
- package/src/layout/Divider.tsx +61 -0
- package/src/layout/Flex.tsx +79 -0
- package/src/layout/Grid.tsx +124 -0
- package/src/layout/PageLayout.tsx +59 -0
- package/src/layout/ScrollArea.tsx +43 -0
- package/src/layout/Section.tsx +47 -0
- package/src/layout/Spacer.tsx +30 -0
- package/src/layout/Stack.tsx +88 -0
- package/src/layout/index.ts +10 -0
- package/src/layout/tokens.css +8 -0
- package/src/navigation/Breadcrumb.tsx +73 -0
- package/src/navigation/CommandPalette.tsx +142 -0
- package/src/navigation/DropdownMenu.tsx +160 -0
- package/src/navigation/Pagination.tsx +114 -0
- package/src/navigation/Stepper.tsx +93 -0
- package/src/navigation/Tabs.tsx +133 -0
- package/src/navigation/index.ts +6 -0
- package/src/navigation/tokens.css +4 -0
- package/src/overlays/AlertDialog.tsx +54 -0
- package/src/overlays/BottomSheet.tsx +123 -0
- package/src/overlays/ContextMenu.tsx +119 -0
- package/src/overlays/Drawer.tsx +145 -0
- package/src/overlays/FocusTrap.tsx +49 -0
- package/src/overlays/HoverCard.tsx +72 -0
- package/src/overlays/Modal.tsx +219 -0
- package/src/overlays/Popover.tsx +126 -0
- package/src/overlays/Portal.tsx +23 -0
- package/src/overlays/Toast.tsx +137 -0
- package/src/overlays/Tooltip.tsx +100 -0
- package/src/overlays/index.ts +11 -0
- package/src/overlays/tokens.css +8 -0
- package/src/styles/tokens.css +2 -0
- package/src/typography/Blockquote.tsx +32 -0
- package/src/typography/Code.tsx +89 -0
- package/src/typography/Heading.tsx +56 -0
- package/src/typography/Kbd.tsx +25 -0
- package/src/typography/Label.tsx +47 -0
- package/src/typography/Link.tsx +56 -0
- package/src/typography/Text.tsx +62 -0
- package/src/typography/index.ts +7 -0
- package/src/typography/tokens.css +5 -0
- package/src/utils/cn.ts +6 -0
- package/ui-contract.json +62 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import React, { forwardRef, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Calendar as CalendarIcon, Clock, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
import { Button } from '../buttons/Button';
|
|
5
|
+
import { Popover } from '../overlays/Popover';
|
|
6
|
+
|
|
7
|
+
// Utility helper to format date as YYYY-MM-DD
|
|
8
|
+
export function formatDate(d: Date | null | undefined): string {
|
|
9
|
+
if (!d) return '';
|
|
10
|
+
const year = d.getFullYear();
|
|
11
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
12
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
13
|
+
return `${year}-${month}-${day}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatDisplayDate(d: Date | null | undefined): string {
|
|
17
|
+
if (!d) return '';
|
|
18
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DatePickerProps {
|
|
22
|
+
value?: Date | null;
|
|
23
|
+
defaultValue?: Date | null;
|
|
24
|
+
onChange?: (date: Date | null) => void;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
minDate?: Date;
|
|
27
|
+
maxDate?: Date;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
clearable?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
presets?: { label: string; date: Date }[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
|
35
|
+
(
|
|
36
|
+
{
|
|
37
|
+
value: controlledValue,
|
|
38
|
+
defaultValue,
|
|
39
|
+
onChange,
|
|
40
|
+
placeholder = 'Select date...',
|
|
41
|
+
minDate,
|
|
42
|
+
maxDate,
|
|
43
|
+
disabled = false,
|
|
44
|
+
clearable = true,
|
|
45
|
+
className,
|
|
46
|
+
presets,
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) => {
|
|
50
|
+
const [internalDate, setInternalDate] = useState<Date | null>(
|
|
51
|
+
defaultValue !== undefined ? defaultValue : null
|
|
52
|
+
);
|
|
53
|
+
const selectedDate = controlledValue !== undefined ? controlledValue : internalDate;
|
|
54
|
+
|
|
55
|
+
const [viewDate, setViewDate] = useState<Date>(selectedDate || new Date());
|
|
56
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (selectedDate) {
|
|
60
|
+
setViewDate(selectedDate);
|
|
61
|
+
}
|
|
62
|
+
}, [selectedDate]);
|
|
63
|
+
|
|
64
|
+
const handleSelectDate = (d: Date) => {
|
|
65
|
+
setInternalDate(d);
|
|
66
|
+
onChange?.(d);
|
|
67
|
+
setIsOpen(false);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
setInternalDate(null);
|
|
73
|
+
onChange?.(null);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const prevMonth = () => {
|
|
77
|
+
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const nextMonth = () => {
|
|
81
|
+
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Calendar generation logic
|
|
85
|
+
const year = viewDate.getFullYear();
|
|
86
|
+
const month = viewDate.getMonth();
|
|
87
|
+
const firstDayIndex = new Date(year, month, 1).getDay();
|
|
88
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
89
|
+
const daysInPrevMonth = new Date(year, month, 0).getDate();
|
|
90
|
+
|
|
91
|
+
const days: { date: Date; currentMonth: boolean; disabled: boolean }[] = [];
|
|
92
|
+
|
|
93
|
+
// Prev month padding days
|
|
94
|
+
for (let i = firstDayIndex - 1; i >= 0; i--) {
|
|
95
|
+
const d = new Date(year, month - 1, daysInPrevMonth - i);
|
|
96
|
+
const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
|
|
97
|
+
days.push({ date: d, currentMonth: false, disabled: Boolean(isDis) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Current month days
|
|
101
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
|
102
|
+
const d = new Date(year, month, i);
|
|
103
|
+
const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
|
|
104
|
+
days.push({ date: d, currentMonth: true, disabled: Boolean(isDis) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Next month padding days to complete grid of 35 or 42
|
|
108
|
+
const totalSlots = days.length > 35 ? 42 : 35;
|
|
109
|
+
const remainingSlots = totalSlots - days.length;
|
|
110
|
+
for (let i = 1; i <= remainingSlots; i++) {
|
|
111
|
+
const d = new Date(year, month + 1, i);
|
|
112
|
+
const isDis = (minDate && d < minDate) || (maxDate && d > maxDate);
|
|
113
|
+
days.push({ date: d, currentMonth: false, disabled: Boolean(isDis) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const todayStr = formatDate(new Date());
|
|
117
|
+
const selectedStr = selectedDate ? formatDate(selectedDate) : '';
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div ref={ref} className={cn('relative inline-block w-full', className)}>
|
|
121
|
+
<Popover
|
|
122
|
+
isOpen={isOpen}
|
|
123
|
+
onClose={() => setIsOpen(false)}
|
|
124
|
+
trigger={
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
129
|
+
className={cn(
|
|
130
|
+
'w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm transition-all shadow-inner cursor-pointer',
|
|
131
|
+
disabled
|
|
132
|
+
? 'opacity-50 cursor-not-allowed border-[var(--ui-border-subtle)]'
|
|
133
|
+
: 'border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] focus:ring-2 focus:ring-[var(--ui-primary)]/50'
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
<div className="flex items-center gap-2.5 truncate">
|
|
137
|
+
<CalendarIcon className="w-4 h-4 text-[var(--ui-text-muted)] shrink-0" />
|
|
138
|
+
<span className={selectedDate ? 'text-[var(--ui-text-primary)] font-medium' : 'text-[var(--ui-text-muted)]'}>
|
|
139
|
+
{selectedDate ? formatDisplayDate(selectedDate) : placeholder}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex items-center gap-1 text-[var(--ui-text-muted)]">
|
|
144
|
+
{clearable && selectedDate && !disabled && (
|
|
145
|
+
<span
|
|
146
|
+
onClick={handleClear}
|
|
147
|
+
className="p-1 rounded-full hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] transition-colors"
|
|
148
|
+
>
|
|
149
|
+
<X className="w-3.5 h-3.5" />
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</button>
|
|
154
|
+
}
|
|
155
|
+
>
|
|
156
|
+
<div className="p-4 w-72 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
|
|
157
|
+
{/* Header: Month Year + Prev/Next */}
|
|
158
|
+
<div className="flex items-center justify-between">
|
|
159
|
+
<span className="text-xs font-bold text-[var(--ui-text-primary)] font-mono uppercase tracking-wider">
|
|
160
|
+
{viewDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
|
161
|
+
</span>
|
|
162
|
+
<div className="flex items-center gap-1">
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={prevMonth}
|
|
166
|
+
className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors"
|
|
167
|
+
>
|
|
168
|
+
<ChevronLeft className="w-4 h-4" />
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={nextMonth}
|
|
173
|
+
className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors"
|
|
174
|
+
>
|
|
175
|
+
<ChevronRight className="w-4 h-4" />
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Presets if provided */}
|
|
181
|
+
{presets && presets.length > 0 && (
|
|
182
|
+
<div className="flex flex-wrap gap-1 pb-2 border-b border-[var(--ui-border-subtle)]">
|
|
183
|
+
{presets.map((preset, idx) => (
|
|
184
|
+
<button
|
|
185
|
+
key={idx}
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={() => handleSelectDate(preset.date)}
|
|
188
|
+
className="px-2 py-0.5 rounded-md text-[10px] font-semibold bg-[var(--ui-bg-secondary)] hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-secondary)] transition-colors"
|
|
189
|
+
>
|
|
190
|
+
{preset.label}
|
|
191
|
+
</button>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Weekday headers */}
|
|
197
|
+
<div className="grid grid-cols-7 text-center text-[10px] font-bold text-[var(--ui-text-muted)] font-mono">
|
|
198
|
+
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d) => (
|
|
199
|
+
<div key={d} className="py-1">
|
|
200
|
+
{d}
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Day grid */}
|
|
206
|
+
<div className="grid grid-cols-7 gap-1">
|
|
207
|
+
{days.map((item, idx) => {
|
|
208
|
+
const itemStr = formatDate(item.date);
|
|
209
|
+
const isSelected = itemStr === selectedStr;
|
|
210
|
+
const isToday = itemStr === todayStr;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<button
|
|
214
|
+
key={idx}
|
|
215
|
+
type="button"
|
|
216
|
+
disabled={item.disabled}
|
|
217
|
+
onClick={() => handleSelectDate(item.date)}
|
|
218
|
+
className={cn(
|
|
219
|
+
'h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center transition-all cursor-pointer',
|
|
220
|
+
item.disabled && 'opacity-25 cursor-not-allowed pointer-events-none',
|
|
221
|
+
!item.currentMonth && 'text-[var(--ui-text-muted)] opacity-50',
|
|
222
|
+
isSelected
|
|
223
|
+
? 'bg-[var(--ui-primary)] text-white font-bold shadow-md shadow-blue-500/30'
|
|
224
|
+
: isToday
|
|
225
|
+
? 'border border-[var(--ui-primary)] text-[var(--ui-primary)]'
|
|
226
|
+
: 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{item.date.getDate()}
|
|
230
|
+
</button>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</Popover>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
DatePicker.displayName = 'DatePicker';
|
|
241
|
+
|
|
242
|
+
/* -------------------------------------------------------------------------------------------------
|
|
243
|
+
* DateRangePicker Component
|
|
244
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
245
|
+
export interface DateRange {
|
|
246
|
+
from: Date | null;
|
|
247
|
+
to: Date | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface DateRangePickerProps {
|
|
251
|
+
value?: DateRange;
|
|
252
|
+
defaultValue?: DateRange;
|
|
253
|
+
onChange?: (range: DateRange) => void;
|
|
254
|
+
placeholder?: string;
|
|
255
|
+
className?: string;
|
|
256
|
+
disabled?: boolean;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
|
260
|
+
({ value: controlledVal, defaultValue, onChange, placeholder = 'Select date range...', className, disabled = false }, ref) => {
|
|
261
|
+
const [internalRange, setInternalRange] = useState<DateRange>(
|
|
262
|
+
defaultValue || { from: null, to: null }
|
|
263
|
+
);
|
|
264
|
+
const range = controlledVal || internalRange;
|
|
265
|
+
const [viewDate, setViewDate] = useState<Date>(range.from || new Date());
|
|
266
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
267
|
+
|
|
268
|
+
const handleDayClick = (d: Date) => {
|
|
269
|
+
let next: DateRange;
|
|
270
|
+
if (!range.from || (range.from && range.to)) {
|
|
271
|
+
next = { from: d, to: null };
|
|
272
|
+
} else {
|
|
273
|
+
if (d < range.from) {
|
|
274
|
+
next = { from: d, to: range.from };
|
|
275
|
+
} else {
|
|
276
|
+
next = { from: range.from, to: d };
|
|
277
|
+
setIsOpen(false);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
setInternalRange(next);
|
|
281
|
+
onChange?.(next);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const displayText =
|
|
285
|
+
range.from && range.to
|
|
286
|
+
? `${formatDisplayDate(range.from)} – ${formatDisplayDate(range.to)}`
|
|
287
|
+
: range.from
|
|
288
|
+
? `${formatDisplayDate(range.from)} – ...`
|
|
289
|
+
: placeholder;
|
|
290
|
+
|
|
291
|
+
// Calendar generation
|
|
292
|
+
const year = viewDate.getFullYear();
|
|
293
|
+
const month = viewDate.getMonth();
|
|
294
|
+
const firstDayIndex = new Date(year, month, 1).getDay();
|
|
295
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
296
|
+
|
|
297
|
+
const days: Date[] = [];
|
|
298
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
|
299
|
+
days.push(new Date(year, month, i));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div ref={ref} className={cn('relative inline-block w-full', className)}>
|
|
304
|
+
<Popover
|
|
305
|
+
isOpen={isOpen}
|
|
306
|
+
onClose={() => setIsOpen(false)}
|
|
307
|
+
trigger={
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
disabled={disabled}
|
|
311
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
312
|
+
className="w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm shadow-inner cursor-pointer"
|
|
313
|
+
>
|
|
314
|
+
<div className="flex items-center gap-2.5 truncate">
|
|
315
|
+
<CalendarIcon className="w-4 h-4 text-[var(--ui-text-muted)] shrink-0" />
|
|
316
|
+
<span className={range.from ? 'text-[var(--ui-text-primary)] font-medium' : 'text-[var(--ui-text-muted)]'}>
|
|
317
|
+
{displayText}
|
|
318
|
+
</span>
|
|
319
|
+
</div>
|
|
320
|
+
</button>
|
|
321
|
+
}
|
|
322
|
+
>
|
|
323
|
+
<div className="p-4 w-76 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
|
|
324
|
+
<div className="flex items-center justify-between">
|
|
325
|
+
<span className="text-xs font-bold text-[var(--ui-text-primary)] font-mono uppercase tracking-wider">
|
|
326
|
+
{viewDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
|
327
|
+
</span>
|
|
328
|
+
<div className="flex items-center gap-1">
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
onClick={() => setViewDate(new Date(year, month - 1, 1))}
|
|
332
|
+
className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)]"
|
|
333
|
+
>
|
|
334
|
+
<ChevronLeft className="w-4 h-4" />
|
|
335
|
+
</button>
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
onClick={() => setViewDate(new Date(year, month + 1, 1))}
|
|
339
|
+
className="p-1 rounded-lg hover:bg-[var(--ui-bg-tertiary)]"
|
|
340
|
+
>
|
|
341
|
+
<ChevronRight className="w-4 h-4" />
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div className="grid grid-cols-7 gap-1 text-center">
|
|
347
|
+
{Array.from({ length: firstDayIndex }).map((_, i) => (
|
|
348
|
+
<div key={`empty-${i}`} />
|
|
349
|
+
))}
|
|
350
|
+
{days.map((d, idx) => {
|
|
351
|
+
const dStr = formatDate(d);
|
|
352
|
+
const fromStr = range.from ? formatDate(range.from) : '';
|
|
353
|
+
const toStr = range.to ? formatDate(range.to) : '';
|
|
354
|
+
const isFrom = dStr === fromStr;
|
|
355
|
+
const isTo = dStr === toStr;
|
|
356
|
+
const isInRange =
|
|
357
|
+
range.from && range.to && d > range.from && d < range.to;
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<button
|
|
361
|
+
key={idx}
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => handleDayClick(d)}
|
|
364
|
+
className={cn(
|
|
365
|
+
'h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center transition-all cursor-pointer',
|
|
366
|
+
(isFrom || isTo) && 'bg-[var(--ui-primary)] text-white shadow-md',
|
|
367
|
+
isInRange && 'bg-[var(--ui-primary)]/20 text-[var(--ui-primary)] rounded-none',
|
|
368
|
+
!isFrom && !isTo && !isInRange && 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
|
|
369
|
+
)}
|
|
370
|
+
>
|
|
371
|
+
{d.getDate()}
|
|
372
|
+
</button>
|
|
373
|
+
);
|
|
374
|
+
})}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</Popover>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
DateRangePicker.displayName = 'DateRangePicker';
|
|
383
|
+
|
|
384
|
+
/* -------------------------------------------------------------------------------------------------
|
|
385
|
+
* TimePicker Component
|
|
386
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
387
|
+
export interface TimePickerProps {
|
|
388
|
+
value?: string;
|
|
389
|
+
defaultValue?: string;
|
|
390
|
+
onChange?: (time: string) => void;
|
|
391
|
+
format?: '12h' | '24h';
|
|
392
|
+
className?: string;
|
|
393
|
+
disabled?: boolean;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|
397
|
+
({ value: controlledVal, defaultValue = '12:00', onChange, format = '24h', className, disabled = false }, ref) => {
|
|
398
|
+
const [internalTime, setInternalTime] = useState(defaultValue);
|
|
399
|
+
const time = controlledVal !== undefined ? controlledVal : internalTime;
|
|
400
|
+
|
|
401
|
+
const [hours, setHours] = useState(time.split(':')[0] || '12');
|
|
402
|
+
const [minutes, setMinutes] = useState(time.split(':')[1] || '00');
|
|
403
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
404
|
+
|
|
405
|
+
const updateTime = (newH: string, newM: string) => {
|
|
406
|
+
const full = `${newH.padStart(2, '0')}:${newM.padStart(2, '0')}`;
|
|
407
|
+
setHours(newH);
|
|
408
|
+
setMinutes(newM);
|
|
409
|
+
setInternalTime(full);
|
|
410
|
+
onChange?.(full);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div ref={ref} className={cn('relative inline-block w-full', className)}>
|
|
415
|
+
<Popover
|
|
416
|
+
isOpen={isOpen}
|
|
417
|
+
onClose={() => setIsOpen(false)}
|
|
418
|
+
trigger={
|
|
419
|
+
<button
|
|
420
|
+
type="button"
|
|
421
|
+
disabled={disabled}
|
|
422
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
423
|
+
className="w-full h-10 px-3.5 rounded-[var(--ui-radius-md)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] bg-[var(--ui-bg-surface)] text-left flex items-center justify-between text-sm shadow-inner cursor-pointer"
|
|
424
|
+
>
|
|
425
|
+
<div className="flex items-center gap-2.5">
|
|
426
|
+
<Clock className="w-4 h-4 text-[var(--ui-text-muted)]" />
|
|
427
|
+
<span className="font-mono text-sm font-semibold text-[var(--ui-text-primary)]">{time}</span>
|
|
428
|
+
</div>
|
|
429
|
+
</button>
|
|
430
|
+
}
|
|
431
|
+
>
|
|
432
|
+
<div className="p-4 w-52 rounded-2xl bg-[var(--ui-bg-surface)] backdrop-blur-2xl border border-[var(--ui-border-default)] shadow-2xl space-y-3">
|
|
433
|
+
<span className="text-xs font-bold text-[var(--ui-text-muted)] uppercase tracking-wider font-mono">
|
|
434
|
+
Select Time ({format})
|
|
435
|
+
</span>
|
|
436
|
+
<div className="flex items-center justify-center gap-2">
|
|
437
|
+
<div className="h-36 overflow-y-auto border border-[var(--ui-border-subtle)] rounded-xl p-1 w-16 text-center space-y-1">
|
|
438
|
+
{Array.from({ length: 24 }).map((_, i) => {
|
|
439
|
+
const h = String(i).padStart(2, '0');
|
|
440
|
+
return (
|
|
441
|
+
<div
|
|
442
|
+
key={h}
|
|
443
|
+
onClick={() => updateTime(h, minutes)}
|
|
444
|
+
className={cn(
|
|
445
|
+
'text-xs py-1 rounded-lg font-mono font-semibold cursor-pointer transition-colors',
|
|
446
|
+
hours === h
|
|
447
|
+
? 'bg-[var(--ui-primary)] text-white'
|
|
448
|
+
: 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
{h}
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
})}
|
|
455
|
+
</div>
|
|
456
|
+
<span className="text-sm font-bold">:</span>
|
|
457
|
+
<div className="h-36 overflow-y-auto border border-[var(--ui-border-subtle)] rounded-xl p-1 w-16 text-center space-y-1">
|
|
458
|
+
{['00', '05', '10', '15', '20', '25', '30', '35', '40', '41', '45', '50', '55'].map((m) => (
|
|
459
|
+
<div
|
|
460
|
+
key={m}
|
|
461
|
+
onClick={() => updateTime(hours, m)}
|
|
462
|
+
className={cn(
|
|
463
|
+
'text-xs py-1 rounded-lg font-mono font-semibold cursor-pointer transition-colors',
|
|
464
|
+
minutes === m
|
|
465
|
+
? 'bg-[var(--ui-primary)] text-white'
|
|
466
|
+
: 'hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)]'
|
|
467
|
+
)}
|
|
468
|
+
>
|
|
469
|
+
{m}
|
|
470
|
+
</div>
|
|
471
|
+
))}
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</Popover>
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
TimePicker.displayName = 'TimePicker';
|