@shipfox/react-ui 0.11.0 → 0.12.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/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-check.log +2 -2
- package/.turbo/turbo-type.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/components/calendar/calendar.d.ts +5 -0
- package/dist/components/calendar/calendar.d.ts.map +1 -0
- package/dist/components/calendar/calendar.js +46 -0
- package/dist/components/calendar/calendar.js.map +1 -0
- package/dist/components/calendar/index.d.ts +2 -0
- package/dist/components/calendar/index.d.ts.map +1 -0
- package/dist/components/calendar/index.js +3 -0
- package/dist/components/calendar/index.js.map +1 -0
- package/dist/components/date-picker/date-picker.d.ts +19 -0
- package/dist/components/date-picker/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker/date-picker.js +114 -0
- package/dist/components/date-picker/date-picker.js.map +1 -0
- package/dist/components/date-picker/date-picker.stories.js +333 -0
- package/dist/components/date-picker/date-picker.stories.js.map +1 -0
- package/dist/components/date-picker/index.d.ts +2 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +3 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.d.ts +24 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.d.ts.map +1 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.js +130 -0
- package/dist/components/date-time-range-picker/date-time-range-picker.js.map +1 -0
- package/dist/components/date-time-range-picker/index.d.ts +2 -0
- package/dist/components/date-time-range-picker/index.d.ts.map +1 -0
- package/dist/components/date-time-range-picker/index.js +3 -0
- package/dist/components/date-time-range-picker/index.js.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/inline-tips/inline-tips.stories.js +5 -0
- package/dist/components/inline-tips/inline-tips.stories.js.map +1 -1
- package/dist/components/item/item.stories.js +15 -8
- package/dist/components/item/item.stories.js.map +1 -1
- package/dist/components/modal/modal.stories.js +14 -6
- package/dist/components/modal/modal.stories.js.map +1 -1
- package/dist/components/popover/index.d.ts +2 -0
- package/dist/components/popover/index.d.ts.map +1 -0
- package/dist/components/popover/index.js +3 -0
- package/dist/components/popover/index.js.map +1 -0
- package/dist/components/popover/popover.d.ts +10 -0
- package/dist/components/popover/popover.d.ts.map +1 -0
- package/dist/components/popover/popover.js +47 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/textarea/textarea.stories.js +8 -2
- package/dist/components/textarea/textarea.stories.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/calendar/calendar.tsx +90 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/date-picker/date-picker.stories.tsx +230 -0
- package/src/components/date-picker/date-picker.tsx +179 -0
- package/src/components/date-picker/index.ts +1 -0
- package/src/components/date-time-range-picker/date-time-range-picker.tsx +211 -0
- package/src/components/date-time-range-picker/index.ts +1 -0
- package/src/components/index.ts +4 -0
- package/src/components/inline-tips/inline-tips.stories.tsx +5 -0
- package/src/components/item/item.stories.tsx +65 -56
- package/src/components/modal/modal.stories.tsx +16 -6
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.tsx +60 -0
- package/src/components/textarea/textarea.stories.tsx +8 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {argosScreenshot} from '@argos-ci/storybook/vitest';
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react';
|
|
3
|
+
import {within} from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import {subDays} from 'date-fns';
|
|
6
|
+
import {useState} from 'react';
|
|
7
|
+
import {
|
|
8
|
+
type DateRange,
|
|
9
|
+
DateTimeRangePicker,
|
|
10
|
+
} from '../date-time-range-picker/date-time-range-picker';
|
|
11
|
+
import {DatePicker} from './date-picker';
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof DatePicker> = {
|
|
14
|
+
title: 'Components/DatePicker',
|
|
15
|
+
component: DatePicker,
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'centered',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof DatePicker>;
|
|
23
|
+
|
|
24
|
+
const OPEN_CALENDAR_REGEX = /open calendar/i;
|
|
25
|
+
|
|
26
|
+
const isTestEnvironment = () => typeof navigator !== 'undefined' && navigator.webdriver === true;
|
|
27
|
+
|
|
28
|
+
type StoryContext = Parameters<NonNullable<Story['play']>>[0];
|
|
29
|
+
|
|
30
|
+
async function openCalendarAndScreenshot(ctx: StoryContext, screenshotName: string): Promise<void> {
|
|
31
|
+
const {canvasElement, step} = ctx;
|
|
32
|
+
const canvas = within(canvasElement);
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
|
|
35
|
+
let triggerButton: HTMLElement | null = null;
|
|
36
|
+
|
|
37
|
+
await step('Open the calendar popover', async () => {
|
|
38
|
+
triggerButton = canvas.getByRole('button', {name: OPEN_CALENDAR_REGEX});
|
|
39
|
+
await user.click(triggerButton);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await step('Wait for calendar to appear and render', async () => {
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
44
|
+
|
|
45
|
+
if (isTestEnvironment() && triggerButton instanceof HTMLElement) {
|
|
46
|
+
triggerButton.style.display = 'none';
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await argosScreenshot(ctx, screenshotName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DatePickerStory: Story = {
|
|
55
|
+
play: (ctx) => openCalendarAndScreenshot(ctx, 'DatePicker Calendar Open'),
|
|
56
|
+
render: () => {
|
|
57
|
+
const [date, setDate] = useState<Date | undefined>(new Date());
|
|
58
|
+
return (
|
|
59
|
+
<div className="relative flex h-600 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible">
|
|
60
|
+
<DatePicker
|
|
61
|
+
date={date}
|
|
62
|
+
onDateSelect={setDate}
|
|
63
|
+
onClear={() => setDate(undefined)}
|
|
64
|
+
placeholder="DD/MM/YYYY"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const DateRangePickerStory: Story = {
|
|
72
|
+
play: (ctx) => openCalendarAndScreenshot(ctx, 'DateRangePicker Calendar Open'),
|
|
73
|
+
render: () => {
|
|
74
|
+
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
|
75
|
+
start: new Date(),
|
|
76
|
+
end: subDays(new Date(), -30),
|
|
77
|
+
});
|
|
78
|
+
return (
|
|
79
|
+
<div className="relative flex h-600 w-800 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible">
|
|
80
|
+
<DateTimeRangePicker
|
|
81
|
+
dateRange={dateRange}
|
|
82
|
+
onDateRangeSelect={setDateRange}
|
|
83
|
+
onClear={() => setDateRange(undefined)}
|
|
84
|
+
placeholder="DD/MM/YYYY - DD/MM/YYYY"
|
|
85
|
+
className="min-w-280"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const AllStates: Story = {
|
|
93
|
+
render: () => {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const past = subDays(now, 30);
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-32 p-32 min-w-350">
|
|
98
|
+
<div>
|
|
99
|
+
<h3 className="text-lg font-semibold mb-16 text-foreground-neutral-base">
|
|
100
|
+
Single Date Picker
|
|
101
|
+
</h3>
|
|
102
|
+
<div className="flex flex-col gap-16">
|
|
103
|
+
<div>
|
|
104
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">DEFAULT</p>
|
|
105
|
+
<DatePicker placeholder="DD/MM/YYYY" />
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">FILLED</p>
|
|
109
|
+
<DatePicker
|
|
110
|
+
date={new Date()}
|
|
111
|
+
onClear={() => {
|
|
112
|
+
/* noop for demo */
|
|
113
|
+
}}
|
|
114
|
+
placeholder="DD/MM/YYYY"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">ERROR</p>
|
|
119
|
+
<DatePicker
|
|
120
|
+
date={new Date()}
|
|
121
|
+
onClear={() => {
|
|
122
|
+
/* noop for demo */
|
|
123
|
+
}}
|
|
124
|
+
state="error"
|
|
125
|
+
placeholder="DD/MM/YYYY"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">DISABLED</p>
|
|
130
|
+
<DatePicker placeholder="DD/MM/YYYY" state="disabled" />
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">SMALL</p>
|
|
134
|
+
<DatePicker size="small" placeholder="DD/MM/YYYY" />
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">
|
|
138
|
+
COMPONENT VARIANT
|
|
139
|
+
</p>
|
|
140
|
+
<DatePicker variant="component" placeholder="DD/MM/YYYY" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div>
|
|
146
|
+
<h3 className="text-lg font-semibold mb-16 text-foreground-neutral-base">
|
|
147
|
+
Date Range Picker
|
|
148
|
+
</h3>
|
|
149
|
+
<div className="flex flex-col gap-16">
|
|
150
|
+
<div>
|
|
151
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">DEFAULT</p>
|
|
152
|
+
<DateTimeRangePicker placeholder="DD/MM/YYYY - DD/MM/YYYY" />
|
|
153
|
+
</div>
|
|
154
|
+
<div>
|
|
155
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">FILLED</p>
|
|
156
|
+
<DateTimeRangePicker
|
|
157
|
+
dateRange={{start: past, end: now}}
|
|
158
|
+
onClear={() => {
|
|
159
|
+
/* noop for demo */
|
|
160
|
+
}}
|
|
161
|
+
placeholder="DD/MM/YYYY - DD/MM/YYYY"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">ERROR</p>
|
|
166
|
+
<DateTimeRangePicker
|
|
167
|
+
dateRange={{start: past, end: now}}
|
|
168
|
+
onClear={() => {
|
|
169
|
+
/* noop for demo */
|
|
170
|
+
}}
|
|
171
|
+
state="error"
|
|
172
|
+
placeholder="DD/MM/YYYY - DD/MM/YYYY"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<div>
|
|
176
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">DISABLED</p>
|
|
177
|
+
<DateTimeRangePicker placeholder="DD/MM/YYYY - DD/MM/YYYY" state="disabled" />
|
|
178
|
+
</div>
|
|
179
|
+
<div>
|
|
180
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">SMALL</p>
|
|
181
|
+
<DateTimeRangePicker size="small" placeholder="DD/MM/YYYY - DD/MM/YYYY" />
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">
|
|
185
|
+
COMPONENT VARIANT
|
|
186
|
+
</p>
|
|
187
|
+
<DateTimeRangePicker variant="component" placeholder="DD/MM/YYYY - DD/MM/YYYY" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const DateFormats: Story = {
|
|
197
|
+
render: () => {
|
|
198
|
+
const [date, setDate] = useState<Date | undefined>(new Date());
|
|
199
|
+
const formats = [
|
|
200
|
+
{format: 'MMMM dd, yyyy', placeholder: 'Month DD, YYYY', label: 'MMMM dd, yyyy'},
|
|
201
|
+
{format: 'MMMM d, yyyy', placeholder: 'Month D, YYYY', label: 'MMMM d, yyyy'},
|
|
202
|
+
{format: 'MMM dd, yyyy', placeholder: 'Mon DD, YYYY', label: 'MMM dd, yyyy'},
|
|
203
|
+
{format: 'dd/MM/yyyy', placeholder: 'DD/MM/YYYY', label: 'dd/MM/yyyy'},
|
|
204
|
+
{format: 'MM/dd/yyyy', placeholder: 'MM/DD/YYYY', label: 'MM/dd/yyyy'},
|
|
205
|
+
{format: 'yyyy-MM-dd', placeholder: 'YYYY-MM-DD', label: 'yyyy-MM-dd'},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex flex-col gap-32 p-32 min-w-350">
|
|
210
|
+
<h3 className="text-lg font-semibold mb-16 text-foreground-neutral-base">
|
|
211
|
+
Date Format Examples
|
|
212
|
+
</h3>
|
|
213
|
+
<div className="flex flex-col gap-16">
|
|
214
|
+
{formats.map(({format, placeholder, label}) => (
|
|
215
|
+
<div key={format}>
|
|
216
|
+
<p className="text-xs text-foreground-neutral-subtle mb-8 font-mono">{label}</p>
|
|
217
|
+
<DatePicker
|
|
218
|
+
date={date}
|
|
219
|
+
onDateSelect={setDate}
|
|
220
|
+
onClear={() => setDate(undefined)}
|
|
221
|
+
dateFormat={format}
|
|
222
|
+
placeholder={placeholder}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {cva, type VariantProps} from 'class-variance-authority';
|
|
2
|
+
import {Calendar} from 'components/calendar';
|
|
3
|
+
import {Icon} from 'components/icon';
|
|
4
|
+
import {Popover, PopoverContent, PopoverTrigger} from 'components/popover';
|
|
5
|
+
import {format} from 'date-fns';
|
|
6
|
+
import type {ComponentProps, ReactNode} from 'react';
|
|
7
|
+
import {forwardRef, useState} from 'react';
|
|
8
|
+
import {cn} from 'utils/cn';
|
|
9
|
+
|
|
10
|
+
export const datePickerVariants = cva(
|
|
11
|
+
'relative flex items-center rounded-6 shadow-button-neutral transition-[background-color,box-shadow] outline-none',
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
base: 'bg-background-field-base hover:bg-background-field-hover',
|
|
16
|
+
component: 'bg-background-field-component hover:bg-background-field-component-hover',
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
base: 'h-32',
|
|
20
|
+
small: 'h-28',
|
|
21
|
+
},
|
|
22
|
+
state: {
|
|
23
|
+
default: '',
|
|
24
|
+
error: 'shadow-border-error',
|
|
25
|
+
disabled:
|
|
26
|
+
'bg-background-neutral-disabled shadow-none pointer-events-none cursor-not-allowed',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: 'base',
|
|
31
|
+
size: 'base',
|
|
32
|
+
state: 'default',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export type DatePickerProps = Omit<ComponentProps<'input'>, 'size' | 'type'> &
|
|
38
|
+
VariantProps<typeof datePickerVariants> & {
|
|
39
|
+
date?: Date;
|
|
40
|
+
onDateSelect?: (date: Date | undefined) => void;
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
dateFormat?: string;
|
|
43
|
+
leftIcon?: ReactNode;
|
|
44
|
+
rightIcon?: ReactNode;
|
|
45
|
+
onClear?: () => void;
|
|
46
|
+
closeOnSelect?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
className,
|
|
53
|
+
variant,
|
|
54
|
+
size,
|
|
55
|
+
state,
|
|
56
|
+
date,
|
|
57
|
+
onDateSelect,
|
|
58
|
+
placeholder = 'DD/MM/YYYY',
|
|
59
|
+
dateFormat = 'dd/MM/yyyy',
|
|
60
|
+
leftIcon,
|
|
61
|
+
rightIcon,
|
|
62
|
+
onClear,
|
|
63
|
+
disabled,
|
|
64
|
+
closeOnSelect = false,
|
|
65
|
+
...props
|
|
66
|
+
},
|
|
67
|
+
ref,
|
|
68
|
+
) => {
|
|
69
|
+
const [open, setOpen] = useState(false);
|
|
70
|
+
const isDisabled = disabled || state === 'disabled';
|
|
71
|
+
const displayValue = date ? format(date, dateFormat) : '';
|
|
72
|
+
|
|
73
|
+
const handleSelect = (selectedDate: Date | undefined) => {
|
|
74
|
+
onDateSelect?.(selectedDate);
|
|
75
|
+
if (closeOnSelect) {
|
|
76
|
+
setOpen(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
onClear?.();
|
|
83
|
+
setOpen(false);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
open && 'shadow-border-interactive-with-active',
|
|
91
|
+
datePickerVariants({variant, size, state: isDisabled ? 'disabled' : state}),
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{/* Calendar Icon Button */}
|
|
96
|
+
<PopoverTrigger asChild>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
disabled={isDisabled}
|
|
100
|
+
className={cn(
|
|
101
|
+
'flex items-center justify-center shrink-0 transition-colors',
|
|
102
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
103
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
104
|
+
)}
|
|
105
|
+
aria-label="Open calendar"
|
|
106
|
+
>
|
|
107
|
+
{leftIcon || (
|
|
108
|
+
<Icon
|
|
109
|
+
name="calendar2Line"
|
|
110
|
+
className={cn(
|
|
111
|
+
'size-16 text-foreground-neutral-muted',
|
|
112
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
113
|
+
)}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
</PopoverTrigger>
|
|
118
|
+
|
|
119
|
+
{/* Divider */}
|
|
120
|
+
<div className="h-full w-px bg-border-neutral-base shrink-0" />
|
|
121
|
+
|
|
122
|
+
{/* Input Field */}
|
|
123
|
+
<input
|
|
124
|
+
ref={ref}
|
|
125
|
+
type="text"
|
|
126
|
+
disabled={isDisabled}
|
|
127
|
+
placeholder={placeholder}
|
|
128
|
+
value={displayValue}
|
|
129
|
+
readOnly
|
|
130
|
+
className={cn(
|
|
131
|
+
'flex-1 min-w-0 px-8 text-sm leading-20 bg-transparent outline-none border-none cursor-pointer',
|
|
132
|
+
'placeholder:text-foreground-neutral-muted',
|
|
133
|
+
'text-foreground-neutral-base',
|
|
134
|
+
'disabled:text-foreground-neutral-disabled disabled:cursor-not-allowed',
|
|
135
|
+
size === 'small' ? 'py-4' : 'py-6',
|
|
136
|
+
)}
|
|
137
|
+
onClick={() => !isDisabled && setOpen(true)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
{/* Clear Button (shown when date is selected) */}
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={handleClear}
|
|
145
|
+
className={cn(
|
|
146
|
+
'flex items-center justify-center shrink-0 cursor-pointer',
|
|
147
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
148
|
+
date && onClear && !isDisabled ? 'visible' : 'invisible',
|
|
149
|
+
)}
|
|
150
|
+
aria-label="Clear date"
|
|
151
|
+
>
|
|
152
|
+
<Icon
|
|
153
|
+
name="closeLine"
|
|
154
|
+
className="size-16 text-foreground-neutral-muted hover:text-foreground-neutral-subtle transition-colors"
|
|
155
|
+
/>
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
{/* Custom Right Icon */}
|
|
159
|
+
{rightIcon && !date && (
|
|
160
|
+
<div
|
|
161
|
+
className={cn(
|
|
162
|
+
'flex items-center justify-center shrink-0',
|
|
163
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{rightIcon}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
172
|
+
<Calendar mode="single" selected={date} onSelect={handleSelect} />
|
|
173
|
+
</PopoverContent>
|
|
174
|
+
</Popover>
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
DatePicker.displayName = 'DatePicker';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './date-picker';
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {cva, type VariantProps} from 'class-variance-authority';
|
|
2
|
+
import {Calendar} from 'components/calendar';
|
|
3
|
+
import {Icon} from 'components/icon';
|
|
4
|
+
import {Popover, PopoverContent, PopoverTrigger} from 'components/popover';
|
|
5
|
+
import {format} from 'date-fns';
|
|
6
|
+
import type {ComponentProps, ReactNode} from 'react';
|
|
7
|
+
import {forwardRef, useState} from 'react';
|
|
8
|
+
import type {DateRange as DayPickerDateRange} from 'react-day-picker';
|
|
9
|
+
import {cn} from 'utils/cn';
|
|
10
|
+
|
|
11
|
+
export const dateTimeRangePickerVariants = cva(
|
|
12
|
+
'min-w-240 relative flex items-center rounded-6 shadow-button-neutral transition-[background-color,box-shadow] outline-none',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
base: 'bg-background-field-base hover:bg-background-field-hover',
|
|
17
|
+
component: 'bg-background-field-component hover:bg-background-field-component-hover',
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
base: 'h-32',
|
|
21
|
+
small: 'h-28',
|
|
22
|
+
},
|
|
23
|
+
state: {
|
|
24
|
+
default: '',
|
|
25
|
+
error: 'shadow-border-error',
|
|
26
|
+
disabled:
|
|
27
|
+
'bg-background-neutral-disabled shadow-none pointer-events-none cursor-not-allowed',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: 'base',
|
|
32
|
+
size: 'base',
|
|
33
|
+
state: 'default',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export type DateRange = {
|
|
39
|
+
start?: Date;
|
|
40
|
+
end?: Date;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type DateTimeRangePickerProps = Omit<ComponentProps<'input'>, 'size' | 'type'> &
|
|
44
|
+
VariantProps<typeof dateTimeRangePickerVariants> & {
|
|
45
|
+
dateRange?: DateRange;
|
|
46
|
+
onDateRangeSelect?: (range: DateRange | undefined) => void;
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
dateFormat?: string;
|
|
49
|
+
leftIcon?: ReactNode;
|
|
50
|
+
rightIcon?: ReactNode;
|
|
51
|
+
onClear?: () => void;
|
|
52
|
+
numberOfMonths?: number;
|
|
53
|
+
closeOnSelect?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const DateTimeRangePicker = forwardRef<HTMLInputElement, DateTimeRangePickerProps>(
|
|
57
|
+
(
|
|
58
|
+
{
|
|
59
|
+
className,
|
|
60
|
+
variant,
|
|
61
|
+
size,
|
|
62
|
+
state,
|
|
63
|
+
dateRange,
|
|
64
|
+
onDateRangeSelect,
|
|
65
|
+
placeholder = 'DD/MM/YYYY - DD/MM/YYYY',
|
|
66
|
+
dateFormat = 'dd/MM/yyyy',
|
|
67
|
+
leftIcon,
|
|
68
|
+
rightIcon,
|
|
69
|
+
onClear,
|
|
70
|
+
disabled,
|
|
71
|
+
numberOfMonths = 2,
|
|
72
|
+
closeOnSelect = false,
|
|
73
|
+
...props
|
|
74
|
+
},
|
|
75
|
+
ref,
|
|
76
|
+
) => {
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
const isDisabled = disabled || state === 'disabled';
|
|
79
|
+
const hasRange = dateRange?.start && dateRange?.end;
|
|
80
|
+
const displayValue =
|
|
81
|
+
hasRange && dateRange.start && dateRange.end
|
|
82
|
+
? `${format(dateRange.start, dateFormat)} - ${format(dateRange.end, dateFormat)}`
|
|
83
|
+
: '';
|
|
84
|
+
|
|
85
|
+
// Convert our DateRange to react-day-picker's DateRange format
|
|
86
|
+
const dayPickerRange: DayPickerDateRange | undefined =
|
|
87
|
+
dateRange?.start || dateRange?.end
|
|
88
|
+
? {
|
|
89
|
+
from: dateRange?.start,
|
|
90
|
+
to: dateRange?.end,
|
|
91
|
+
}
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
const handleSelect = (selectedRange: DayPickerDateRange | undefined) => {
|
|
95
|
+
if (selectedRange) {
|
|
96
|
+
onDateRangeSelect?.({
|
|
97
|
+
start: selectedRange.from,
|
|
98
|
+
end: selectedRange.to,
|
|
99
|
+
});
|
|
100
|
+
// Only close if both dates are selected and closeOnSelect is true
|
|
101
|
+
if (closeOnSelect && selectedRange.from && selectedRange.to) {
|
|
102
|
+
setOpen(false);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
onDateRangeSelect?.(undefined);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
onClear?.();
|
|
112
|
+
setOpen(false);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
open && 'shadow-border-interactive-with-active',
|
|
120
|
+
dateTimeRangePickerVariants({variant, size, state: isDisabled ? 'disabled' : state}),
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{/* Calendar Icon Button */}
|
|
125
|
+
<PopoverTrigger asChild>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
disabled={isDisabled}
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex items-center justify-center shrink-0 transition-colors',
|
|
131
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
132
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
133
|
+
)}
|
|
134
|
+
aria-label="Open calendar"
|
|
135
|
+
>
|
|
136
|
+
{leftIcon || (
|
|
137
|
+
<Icon
|
|
138
|
+
name="calendar2Line"
|
|
139
|
+
className={cn(
|
|
140
|
+
'size-16 text-foreground-neutral-muted',
|
|
141
|
+
isDisabled && 'text-foreground-neutral-disabled',
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
</PopoverTrigger>
|
|
147
|
+
|
|
148
|
+
{/* Divider */}
|
|
149
|
+
<div className="h-full w-px bg-border-neutral-base shrink-0" />
|
|
150
|
+
|
|
151
|
+
{/* Input Field */}
|
|
152
|
+
<input
|
|
153
|
+
ref={ref}
|
|
154
|
+
type="text"
|
|
155
|
+
disabled={isDisabled}
|
|
156
|
+
placeholder={placeholder}
|
|
157
|
+
value={displayValue}
|
|
158
|
+
readOnly
|
|
159
|
+
className={cn(
|
|
160
|
+
'flex-1 min-w-0 px-8 text-sm leading-20 bg-transparent outline-none border-none cursor-pointer',
|
|
161
|
+
'placeholder:text-foreground-neutral-muted',
|
|
162
|
+
'text-foreground-neutral-base',
|
|
163
|
+
'disabled:text-foreground-neutral-disabled disabled:cursor-not-allowed',
|
|
164
|
+
size === 'small' ? 'py-4' : 'py-6',
|
|
165
|
+
)}
|
|
166
|
+
onClick={() => !isDisabled && setOpen(true)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
{/* Clear Button (shown when date range is selected) */}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={handleClear}
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex items-center justify-center shrink-0 transition-colors hover:text-foreground-neutral-base',
|
|
176
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
177
|
+
hasRange && onClear && !isDisabled ? 'visible' : 'invisible',
|
|
178
|
+
)}
|
|
179
|
+
aria-label="Clear date range"
|
|
180
|
+
>
|
|
181
|
+
<Icon name="closeLine" className="size-16 text-foreground-neutral-muted" />
|
|
182
|
+
</button>
|
|
183
|
+
|
|
184
|
+
{/* Custom Right Icon */}
|
|
185
|
+
{rightIcon && !hasRange && (
|
|
186
|
+
<div
|
|
187
|
+
className={cn(
|
|
188
|
+
'flex items-center justify-center shrink-0',
|
|
189
|
+
size === 'small' ? 'size-28' : 'size-32',
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{rightIcon}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
198
|
+
<Calendar
|
|
199
|
+
mode="range"
|
|
200
|
+
defaultMonth={dayPickerRange?.from}
|
|
201
|
+
selected={dayPickerRange}
|
|
202
|
+
onSelect={handleSelect}
|
|
203
|
+
numberOfMonths={numberOfMonths}
|
|
204
|
+
/>
|
|
205
|
+
</PopoverContent>
|
|
206
|
+
</Popover>
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
DateTimeRangePicker.displayName = 'DateTimeRangePicker';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './date-time-range-picker';
|
package/src/components/index.ts
CHANGED
|
@@ -2,8 +2,11 @@ export * from './alert';
|
|
|
2
2
|
export * from './avatar';
|
|
3
3
|
export * from './badge';
|
|
4
4
|
export * from './button';
|
|
5
|
+
export * from './calendar';
|
|
5
6
|
export * from './checkbox';
|
|
6
7
|
export * from './code-block';
|
|
8
|
+
export * from './date-picker';
|
|
9
|
+
export * from './date-time-range-picker';
|
|
7
10
|
export * from './dot-grid';
|
|
8
11
|
export * from './dropdown-menu';
|
|
9
12
|
export * from './dynamic-item';
|
|
@@ -14,6 +17,7 @@ export * from './input';
|
|
|
14
17
|
export * from './item';
|
|
15
18
|
export * from './label';
|
|
16
19
|
export * from './modal';
|
|
20
|
+
export * from './popover';
|
|
17
21
|
export * from './textarea';
|
|
18
22
|
export * from './theme';
|
|
19
23
|
export * from './toast';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {argosScreenshot} from '@argos-ci/storybook/vitest';
|
|
1
2
|
import type {Meta, StoryObj} from '@storybook/react';
|
|
2
3
|
import {Code, Header} from 'components/typography';
|
|
3
4
|
import {
|
|
@@ -37,6 +38,10 @@ const types = ['default', 'info', 'success', 'error'] as const;
|
|
|
37
38
|
const variants = ['primary', 'secondary'] as const;
|
|
38
39
|
|
|
39
40
|
export const Default: Story = {
|
|
41
|
+
play: async (ctx) => {
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
43
|
+
await argosScreenshot(ctx, 'InlineTips Default');
|
|
44
|
+
},
|
|
40
45
|
render: (args) => {
|
|
41
46
|
return (
|
|
42
47
|
<InlineTips {...args}>
|