@kkkarsss/ui 1.5.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/button.d.ts +16 -0
- package/dist/components/ui/button.js +33 -0
- package/dist/components/ui/calendar.d.ts +4 -0
- package/dist/components/ui/calendar.js +41 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/popover.js +9 -0
- package/dist/index.css +104 -47
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/ui/controls/date-input/date-input.d.ts +1 -0
- package/dist/ui/controls/date-input/date-input.js +33 -16
- package/dist/ui/controls/date-input/date-input.stories.d.ts +10 -0
- package/dist/ui/controls/date-input/date-input.stories.js +56 -0
- package/dist/ui/controls/date-picker/date-picker.d.ts +7 -2
- package/dist/ui/controls/date-picker/date-picker.js +6 -30
- package/dist/ui/controls/date-picker/date-picker.stories.d.ts +8 -0
- package/dist/ui/controls/date-picker/date-picker.stories.js +46 -0
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +34 -7
- package/dist/ui/information/calendar-like/calendar-like.js +34 -7
- package/dist/ui/information/calendar-like/calendar-like.stories.js +52 -22
- package/dist/ui/information/calendar-like/calendar-slot.js +40 -3
- package/dist/ui/information/calendar-like/types.d.ts +3 -2
- package/dist/ui/information/calendar-like/use-auto-scroll.js +4 -3
- package/dist/ui/information/calendar-like/use-current-time.d.ts +2 -1
- package/dist/ui/information/calendar-like/use-current-time.js +5 -4
- package/dist/ui/information/calendar-like/utils.js +42 -24
- package/package.json +77 -69
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "link" | "secondary" | "default" | "outline" | "destructive" | "ghost" | null | undefined;
|
|
5
|
+
size?: "icon" | "default" | "sm" | "lg" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export type ButtonProps = {
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
|
10
|
+
declare const Button: React.ForwardRefExoticComponent<{
|
|
11
|
+
asChild?: boolean;
|
|
12
|
+
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<(props?: ({
|
|
13
|
+
variant?: "link" | "secondary" | "default" | "outline" | "destructive" | "ghost" | null | undefined;
|
|
14
|
+
size?: "icon" | "default" | "sm" | "lg" | null | undefined;
|
|
15
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
|
|
16
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
const buttonVariants = cva('inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
10
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
11
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
12
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
13
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: 'h-10 px-4 py-2',
|
|
18
|
+
sm: 'h-9 rounded-md px-3',
|
|
19
|
+
lg: 'h-11 rounded-md px-8',
|
|
20
|
+
icon: 'h-10 w-10',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: 'default',
|
|
25
|
+
size: 'default',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
29
|
+
const Comp = asChild ? Slot : 'button';
|
|
30
|
+
return _jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref: ref, ...props });
|
|
31
|
+
});
|
|
32
|
+
Button.displayName = 'Button';
|
|
33
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ru } from 'date-fns/locale';
|
|
4
|
+
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
5
|
+
import { DayPicker } from 'react-day-picker';
|
|
6
|
+
import { buttonVariants } from './button';
|
|
7
|
+
import { cn } from '../../lib/utils';
|
|
8
|
+
function Calendar({ className, classNames, showOutsideDays = true, ...props }) {
|
|
9
|
+
return (_jsx(DayPicker, { locale: ru, showOutsideDays: showOutsideDays, className: cn('p-3 w-fit', className), classNames: {
|
|
10
|
+
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 relative',
|
|
11
|
+
month: 'space-y-4 w-full',
|
|
12
|
+
month_caption: 'flex justify-center pt-1 relative items-center h-9 w-full',
|
|
13
|
+
caption_label: 'text-sm font-medium',
|
|
14
|
+
nav: 'flex items-center',
|
|
15
|
+
button_previous: cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-1 top-1 z-10'),
|
|
16
|
+
button_next: cn(buttonVariants({ variant: 'outline' }), 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-1 top-1 z-10'),
|
|
17
|
+
month_grid: 'w-full border-collapse space-y-1',
|
|
18
|
+
weekdays: 'flex gap-[2px]',
|
|
19
|
+
weekday: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem] flex justify-center items-center',
|
|
20
|
+
week: 'flex w-full mt-[2px] gap-[2px]',
|
|
21
|
+
day: cn('h-9 w-9 p-0 font-normal aria-selected:opacity-100'),
|
|
22
|
+
day_button: cn('h-9 w-9 p-0 font-normal aria-selected:opacity-100 aria-selected:rounded-none'),
|
|
23
|
+
selected: 'bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground aria-selected:rounded-md',
|
|
24
|
+
range_start: '-selected:rounded-l-md aria-selected:bg-primary aria-selected:text-primary-foreground',
|
|
25
|
+
range_end: 'aria-selected:rounded-r-md aria-selected:bg-primary aria-selected:text-primary-foreground',
|
|
26
|
+
range_middle: 'aria-selected:bg-secondary/80 aria-selected:text-accent-foreground',
|
|
27
|
+
today: 'text-accent',
|
|
28
|
+
outside: 'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
|
|
29
|
+
disabled: 'text-muted-foreground opacity-50',
|
|
30
|
+
hidden: 'invisible',
|
|
31
|
+
...classNames,
|
|
32
|
+
}, components: {
|
|
33
|
+
Chevron: ({ ...props }) => {
|
|
34
|
+
if (props.orientation === 'left') {
|
|
35
|
+
return _jsx(ChevronLeftIcon, { className: "h-4 w-4" });
|
|
36
|
+
}
|
|
37
|
+
return _jsx(ChevronRightIcon, { className: "h-4 w-4" });
|
|
38
|
+
},
|
|
39
|
+
}, ...props }));
|
|
40
|
+
}
|
|
41
|
+
export { Calendar };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
declare const Popover: React.FC<PopoverPrimitive.PopoverProps>;
|
|
4
|
+
declare const PopoverTrigger: React.ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
declare const PopoverContent: React.ForwardRefExoticComponent<Omit<PopoverPrimitive.PopoverContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
6
|
+
export { Popover, PopoverTrigger, PopoverContent };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
const Popover = PopoverPrimitive.Root;
|
|
6
|
+
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
7
|
+
const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, className: cn('z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className), ...props }) })));
|
|
8
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
9
|
+
export { Popover, PopoverTrigger, PopoverContent };
|
package/dist/index.css
CHANGED
|
@@ -134,67 +134,131 @@
|
|
|
134
134
|
.gap-xl { gap: 32px; }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
@layer base {
|
|
138
|
+
.dark {
|
|
139
|
+
--background: 0 0% 14%;
|
|
140
|
+
--foreground: 0 0% 85%;
|
|
141
|
+
|
|
142
|
+
--card: 222.2 84% 4.9%;
|
|
143
|
+
--card-foreground: 210 40% 98%;
|
|
144
|
+
|
|
145
|
+
--popover: 222.2 84% 4.9%;
|
|
146
|
+
--popover-foreground: 210 40% 98%;
|
|
147
|
+
|
|
148
|
+
--primary: 210 40% 98%;
|
|
149
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
150
|
+
|
|
151
|
+
--secondary: 0 0% 23%;
|
|
152
|
+
--secondary-foreground: 0 0% 60%;
|
|
153
|
+
|
|
154
|
+
--muted: 217.2 32.6% 17.5%;
|
|
155
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
156
|
+
|
|
157
|
+
--accent: 346 84% 58%;
|
|
158
|
+
--accent-foreground: 0 0% 100%;
|
|
159
|
+
|
|
160
|
+
--destructive: 0 62.8% 30.6%;
|
|
161
|
+
--destructive-foreground: 210 40% 98%;
|
|
162
|
+
|
|
163
|
+
--border: 217.2 32.6% 17.5%;
|
|
164
|
+
--input: 217.2 32.6% 17.5%;
|
|
165
|
+
--ring: 212.7 26.8% 83.9%;
|
|
166
|
+
|
|
167
|
+
--background-accent: #272727;
|
|
168
|
+
--shadow: rgba(0, 0, 0, 0.1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
:root {
|
|
172
|
+
--background: 0 0% 14%;
|
|
173
|
+
--foreground: 0 0% 85%;
|
|
174
|
+
|
|
175
|
+
--card: 222.2 84% 4.9%;
|
|
176
|
+
--card-foreground: 210 40% 98%;
|
|
177
|
+
|
|
178
|
+
--popover: 222.2 84% 4.9%;
|
|
179
|
+
--popover-foreground: 210 40% 98%;
|
|
180
|
+
|
|
181
|
+
--primary: 210 40% 98%;
|
|
182
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
183
|
+
|
|
184
|
+
--secondary: 0 0% 23%;
|
|
185
|
+
--secondary-foreground: 0 0% 60%;
|
|
186
|
+
|
|
187
|
+
--muted: 217.2 32.6% 17.5%;
|
|
188
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
189
|
+
|
|
190
|
+
--accent: 346 84% 58%;
|
|
191
|
+
--accent-foreground: 0 0% 100%;
|
|
192
|
+
|
|
193
|
+
--destructive: 0 62.8% 30.6%;
|
|
194
|
+
--destructive-foreground: 210 40% 98%;
|
|
195
|
+
|
|
196
|
+
--border: 217.2 32.6% 17.5%;
|
|
197
|
+
--input: 217.2 32.6% 17.5%;
|
|
198
|
+
--ring: 212.7 26.8% 83.9%;
|
|
199
|
+
|
|
200
|
+
--radius: 0.5rem;
|
|
201
|
+
|
|
202
|
+
--background-accent: #272727;
|
|
203
|
+
--shadow: rgba(0, 0, 0, 0.1);
|
|
204
|
+
--success: #22c55e;
|
|
205
|
+
--warning: #f59e0b;
|
|
206
|
+
--error: #ef4444;
|
|
139
207
|
|
|
140
|
-
/* Text Sizes */
|
|
141
208
|
--text-xl: 18px;
|
|
142
209
|
--text-l: 16px;
|
|
143
210
|
--text-m: 14px;
|
|
144
211
|
--text-s: 12px;
|
|
145
212
|
--text-xs: 10px;
|
|
213
|
+
}
|
|
146
214
|
|
|
147
|
-
|
|
148
|
-
--background:
|
|
149
|
-
--
|
|
150
|
-
--foreground: #d9d9d9;
|
|
215
|
+
.light {
|
|
216
|
+
--background: 0 0% 96%;
|
|
217
|
+
--foreground: 240 10% 4%;
|
|
151
218
|
|
|
152
|
-
--
|
|
153
|
-
--
|
|
219
|
+
--card: 0 0% 100%;
|
|
220
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
154
221
|
|
|
155
|
-
--
|
|
156
|
-
--
|
|
157
|
-
--shadow: rgba(0, 0, 0, 0.1);
|
|
222
|
+
--popover: 0 0% 100%;
|
|
223
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
158
224
|
|
|
159
|
-
--
|
|
160
|
-
--
|
|
161
|
-
--error: #ef4444;
|
|
225
|
+
--primary: 222.2 47.4% 11.2%;
|
|
226
|
+
--primary-foreground: 210 40% 98%;
|
|
162
227
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
-webkit-font-smoothing: antialiased;
|
|
166
|
-
}
|
|
228
|
+
--secondary: 220 14% 91%;
|
|
229
|
+
--secondary-foreground: 240 0% 57%;
|
|
167
230
|
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
--background: #232323;
|
|
171
|
-
--background-accent: #272727;
|
|
172
|
-
--foreground: #d9d9d9;
|
|
231
|
+
--muted: 210 40% 96.1%;
|
|
232
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
173
233
|
|
|
174
|
-
--
|
|
175
|
-
--
|
|
234
|
+
--accent: 346 84% 58%;
|
|
235
|
+
--accent-foreground: 0 0% 100%;
|
|
176
236
|
|
|
177
|
-
--
|
|
178
|
-
--
|
|
179
|
-
--shadow: rgba(0, 0, 0, 0.1);
|
|
180
|
-
}
|
|
237
|
+
--destructive: 0 84.2% 60.2%;
|
|
238
|
+
--destructive-foreground: 210 40% 98%;
|
|
181
239
|
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
--
|
|
185
|
-
--background-accent: #fafafa;
|
|
186
|
-
--foreground: #080809;
|
|
240
|
+
--border: 214.3 31.8% 91.4%;
|
|
241
|
+
--input: 214.3 31.8% 91.4%;
|
|
242
|
+
--ring: 222.2 84% 4.9%;
|
|
187
243
|
|
|
188
|
-
--
|
|
189
|
-
--secondary-foreground: #919191;
|
|
244
|
+
--background-accent: #fafafa;
|
|
190
245
|
--shadow: rgba(0, 0, 0, 0.03);
|
|
246
|
+
}
|
|
191
247
|
}
|
|
192
248
|
|
|
193
|
-
|
|
249
|
+
@layer base {
|
|
250
|
+
* {
|
|
251
|
+
@apply border-border;
|
|
194
252
|
box-sizing: border-box;
|
|
195
253
|
margin: 0;
|
|
196
254
|
padding: 0;
|
|
197
255
|
font-family: Roboto, sans-serif;
|
|
256
|
+
}
|
|
257
|
+
body {
|
|
258
|
+
@apply bg-background text-foreground;
|
|
259
|
+
width: 100%;
|
|
260
|
+
height: 100vh;
|
|
261
|
+
}
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
.cursor {
|
|
@@ -206,16 +270,9 @@ a {
|
|
|
206
270
|
color: inherit;
|
|
207
271
|
}
|
|
208
272
|
|
|
209
|
-
body {
|
|
210
|
-
width: 100%;
|
|
211
|
-
height: 100vh;
|
|
212
|
-
background-color: var(--background);
|
|
213
|
-
color: var(--foreground);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
273
|
.scrollbar-custom {
|
|
217
274
|
scrollbar-width: thin;
|
|
218
|
-
scrollbar-color: var(--secondary) transparent;
|
|
275
|
+
scrollbar-color: hsl(var(--secondary)) transparent;
|
|
219
276
|
}
|
|
220
277
|
.scrollbar-custom::-webkit-scrollbar {
|
|
221
278
|
width: 4px;
|
|
@@ -225,7 +282,7 @@ body {
|
|
|
225
282
|
background: transparent;
|
|
226
283
|
}
|
|
227
284
|
.scrollbar-custom::-webkit-scrollbar-thumb {
|
|
228
|
-
background-color: var(--secondary);
|
|
285
|
+
background-color: hsl(var(--secondary));
|
|
229
286
|
border-radius: 10px;
|
|
230
287
|
}
|
|
231
288
|
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
3
|
+
import { Calendar as CalendarIcon } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover';
|
|
5
6
|
import { DatePicker } from '../date-picker/date-picker';
|
|
6
7
|
import { Input } from '../input/input';
|
|
7
|
-
export const DateInput = ({ value, onChange, label, placeholder, size = 'm', disabled }) => {
|
|
8
|
+
export const DateInput = ({ value, onChange, label, placeholder, size = 'm', disabled, showTime, }) => {
|
|
8
9
|
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
-
const containerRef = useRef(null);
|
|
10
10
|
const handleDateSelect = (date) => {
|
|
11
11
|
if (date) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const plainDate = Temporal.PlainDate.from({
|
|
13
|
+
year: date.getFullYear(),
|
|
14
|
+
month: date.getMonth() + 1,
|
|
15
|
+
day: date.getDate(),
|
|
16
|
+
});
|
|
17
|
+
if (showTime) {
|
|
18
|
+
// Если уже есть время в value, сохраняем его
|
|
19
|
+
let currentTime = '00:00';
|
|
20
|
+
if (value && value.includes('T')) {
|
|
21
|
+
currentTime = value.split('T')[1].substring(0, 5);
|
|
22
|
+
}
|
|
23
|
+
else if (value && value.includes(':')) {
|
|
24
|
+
currentTime = value.split(' ')[1] || value;
|
|
25
|
+
}
|
|
26
|
+
onChange(`${plainDate.toString()}T${currentTime}`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
onChange(plainDate.toString());
|
|
30
|
+
setIsOpen(false);
|
|
31
|
+
}
|
|
17
32
|
}
|
|
18
33
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const handleTimeChange = (e) => {
|
|
35
|
+
const time = e.target.value;
|
|
36
|
+
const datePart = value.split('T')[0] || new Date().toISOString().split('T')[0];
|
|
37
|
+
onChange(`${datePart}T${time}`);
|
|
38
|
+
};
|
|
39
|
+
const selectedDate = value ? new Date(value.split('T')[0]) : undefined;
|
|
40
|
+
const timeValue = value && value.includes('T') ? value.split('T')[1].substring(0, 5) : '';
|
|
41
|
+
return (_jsx("div", { className: "relative w-full", children: _jsxs(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [_jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Input, { label: label, value: value, onChange: (e) => onChange(e.target.value), type: "text", placeholder: placeholder || (showTime ? 'YYYY-MM-DDTHH:mm' : 'YYYY-MM-DD'), size: size, disabled: disabled, rightAcc: _jsx("div", { className: "flex items-center h-full", children: _jsx(PopoverTrigger, { asChild: true, children: _jsx(CalendarIcon, { size: 16, className: "cursor-pointer text-secondary transition-colors hover:text-accent" }) }) }) }), showTime && (_jsx("div", { className: "w-24", children: _jsx(Input, { type: "text", value: timeValue, onChange: handleTimeChange, placeholder: "HH:mm", size: size, disabled: disabled }) }))] }), _jsx(PopoverContent, { className: "w-auto p-0", align: "end", children: _jsx(DatePicker, { selected: selectedDate, onSelect: handleDateSelect }) })] }) }));
|
|
25
42
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { DateInput } from './date-input';
|
|
3
|
+
declare const meta: Meta<typeof DateInput>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Empty: Story;
|
|
8
|
+
export declare const Sizes: Story;
|
|
9
|
+
export declare const Disabled: Story;
|
|
10
|
+
export declare const WithTime: Story;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { DateInput } from './date-input';
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Controls/DateInput',
|
|
6
|
+
component: DateInput,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
export const Default = {
|
|
11
|
+
render: (args) => {
|
|
12
|
+
const [value, setValue] = useState(args.value);
|
|
13
|
+
return _jsx(DateInput, { ...args, value: value, onChange: setValue });
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
value: '2024-05-20',
|
|
17
|
+
label: 'Выберите дату',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export const Empty = {
|
|
21
|
+
render: (args) => {
|
|
22
|
+
const [value, setValue] = useState(args.value);
|
|
23
|
+
return _jsx(DateInput, { ...args, value: value, onChange: setValue });
|
|
24
|
+
},
|
|
25
|
+
args: {
|
|
26
|
+
value: '',
|
|
27
|
+
label: 'Пустая дата',
|
|
28
|
+
placeholder: 'Выберите день...',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const Sizes = {
|
|
32
|
+
render: (args) => {
|
|
33
|
+
const [v1, setV1] = useState('2024-01-01');
|
|
34
|
+
const [v2, setV2] = useState('2024-01-01');
|
|
35
|
+
const [v3, setV3] = useState('2024-01-01');
|
|
36
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(DateInput, { ...args, size: "s", label: "Small", value: v1, onChange: setV1 }), _jsx(DateInput, { ...args, size: "m", label: "Medium", value: v2, onChange: setV2 }), _jsx(DateInput, { ...args, size: "l", label: "Large", value: v3, onChange: setV3 })] }));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export const Disabled = {
|
|
40
|
+
args: {
|
|
41
|
+
value: '2024-05-20',
|
|
42
|
+
label: 'Заблокировано',
|
|
43
|
+
disabled: true,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
export const WithTime = {
|
|
47
|
+
render: (args) => {
|
|
48
|
+
const [value, setValue] = useState(args.value);
|
|
49
|
+
return _jsx(DateInput, { ...args, value: value, onChange: setValue });
|
|
50
|
+
},
|
|
51
|
+
args: {
|
|
52
|
+
value: '2024-05-20T14:30',
|
|
53
|
+
label: 'С выбором времени',
|
|
54
|
+
showTime: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { FC } from 'react';
|
|
2
|
-
import 'react-day-picker
|
|
2
|
+
import type { DateRange } from 'react-day-picker';
|
|
3
3
|
export type TDatePickerProps = {
|
|
4
|
+
mode?: 'single';
|
|
4
5
|
selected?: Date;
|
|
5
|
-
onSelect
|
|
6
|
+
onSelect?: (date: Date | undefined) => void;
|
|
7
|
+
} | {
|
|
8
|
+
mode: 'range';
|
|
9
|
+
selected?: DateRange;
|
|
10
|
+
onSelect?: (range: DateRange | undefined) => void;
|
|
6
11
|
};
|
|
7
12
|
export declare const DatePicker: FC<TDatePickerProps>;
|
|
@@ -1,32 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
boxShadow: `0 4px 6px var(--shadow)`,
|
|
9
|
-
}, children: _jsx(DayPicker, { mode: "single", selected: selected, onSelect: onSelect, className: "react-day-picker", styles: {
|
|
10
|
-
caption: {
|
|
11
|
-
color: `var(--foreground)`,
|
|
12
|
-
},
|
|
13
|
-
day: {
|
|
14
|
-
borderRadius: '0.375rem',
|
|
15
|
-
color: `var(--foreground)`,
|
|
16
|
-
},
|
|
17
|
-
day_selected: {
|
|
18
|
-
backgroundColor: `var(--accent)`,
|
|
19
|
-
color: `var(--accent-foreground)`,
|
|
20
|
-
},
|
|
21
|
-
day_today: {
|
|
22
|
-
backgroundColor: `var(--background-secondary)`,
|
|
23
|
-
color: `var(--foreground)`,
|
|
24
|
-
},
|
|
25
|
-
day_outside: {
|
|
26
|
-
color: `var(--secondary-foreground)`,
|
|
27
|
-
},
|
|
28
|
-
root: {
|
|
29
|
-
background: `var(--background-accent)`,
|
|
30
|
-
},
|
|
31
|
-
} }) }));
|
|
2
|
+
import { Calendar } from '../../../components/ui/calendar';
|
|
3
|
+
export const DatePicker = (props) => {
|
|
4
|
+
if (props.mode === 'range') {
|
|
5
|
+
return (_jsx(Calendar, { mode: "range", selected: props.selected, onSelect: props.onSelect, className: 'rounded-lg border w-fit' }));
|
|
6
|
+
}
|
|
7
|
+
return (_jsx(Calendar, { mode: "single", selected: props.selected, onSelect: props.onSelect, className: 'rounded-lg border-accent w-fit' }));
|
|
32
8
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { type TDatePickerProps } from './date-picker';
|
|
3
|
+
declare const meta: Meta<TDatePickerProps>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<TDatePickerProps>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Range: Story;
|
|
8
|
+
export declare const Empty: Story;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { addDays } from 'date-fns';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { DatePicker } from './date-picker';
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Controls/DatePicker',
|
|
7
|
+
component: DatePicker,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
export const Default = {
|
|
12
|
+
render: (args) => {
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
14
|
+
const [selected, setSelected] = useState(args.selected);
|
|
15
|
+
return _jsx(DatePicker, { ...args, selected: selected, onSelect: setSelected });
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
mode: 'single',
|
|
19
|
+
selected: new Date(),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export const Range = {
|
|
23
|
+
render: (args) => {
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
25
|
+
const [selected, setSelected] = useState(args.selected);
|
|
26
|
+
return _jsx(DatePicker, { ...args, mode: "range", selected: selected, onSelect: setSelected });
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
mode: 'range',
|
|
30
|
+
selected: {
|
|
31
|
+
from: new Date(),
|
|
32
|
+
to: addDays(new Date(), 7),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export const Empty = {
|
|
37
|
+
render: (args) => {
|
|
38
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
39
|
+
const [selected, setSelected] = useState(args.selected);
|
|
40
|
+
return _jsx(DatePicker, { ...args, selected: selected, onSelect: setSelected });
|
|
41
|
+
},
|
|
42
|
+
args: {
|
|
43
|
+
mode: 'single',
|
|
44
|
+
selected: undefined,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { useState } from 'react';
|
|
3
4
|
import { jc } from '../../../utils';
|
|
4
5
|
export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, onResize, renderTask, }) => {
|
|
@@ -8,12 +9,36 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
8
9
|
e.preventDefault();
|
|
9
10
|
e.stopPropagation();
|
|
10
11
|
const startY = e.clientY;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const toISO = (d) => {
|
|
13
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
14
|
+
return d.toString().split('T')[0];
|
|
15
|
+
if (d instanceof Date)
|
|
16
|
+
return d.toISOString().split('T')[0];
|
|
17
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
18
|
+
const parsed = new Date(d);
|
|
19
|
+
if (!isNaN(parsed.getTime()))
|
|
20
|
+
return parsed.toISOString().split('T')[0];
|
|
21
|
+
}
|
|
22
|
+
return String(d);
|
|
23
|
+
};
|
|
24
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
25
|
+
? task.dueDateStart
|
|
26
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
27
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate
|
|
28
|
+
? task.dueDateEnd
|
|
29
|
+
: Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
30
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
31
|
+
? task.dueTimeStart
|
|
32
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
33
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
34
|
+
? task.dueTimeEnd
|
|
35
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
36
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
37
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
38
|
+
const estimatedTime = Number(dtEnd.since(dtStart).total('minute'));
|
|
14
39
|
const startHeight = (estimatedTime / 15) * 20;
|
|
15
|
-
const startHour =
|
|
16
|
-
const startMinutes =
|
|
40
|
+
const startHour = tStart.hour;
|
|
41
|
+
const startMinutes = tStart.minute;
|
|
17
42
|
const onMouseMove = (moveEvent) => {
|
|
18
43
|
const deltaY = moveEvent.clientY - startY;
|
|
19
44
|
let newHeight;
|
|
@@ -74,8 +99,10 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
74
99
|
const displayHeight = preview ? `${(preview.estimatedTime / 15) * 20}px` : position.height;
|
|
75
100
|
let displayTop = position.top;
|
|
76
101
|
if (preview && (preview.startHour !== undefined || preview.startMinutes !== undefined)) {
|
|
77
|
-
const
|
|
78
|
-
|
|
102
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
103
|
+
? task.dueTimeStart
|
|
104
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
105
|
+
const originalStartTotalMinutes = tStart.hour * 60 + tStart.minute;
|
|
79
106
|
const newStartTotalMinutes = preview.startHour * 60 + preview.startMinutes;
|
|
80
107
|
const diffMinutes = newStartTotalMinutes - originalStartTotalMinutes;
|
|
81
108
|
const diffPixels = (diffMinutes / 15) * 20;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { useMemo, useState, useRef } from 'react';
|
|
3
4
|
import { CalendarItemWrapper } from './calendar-item-wrapper';
|
|
4
5
|
import { CalendarSlot } from './calendar-slot';
|
|
@@ -25,9 +26,31 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
|
|
|
25
26
|
setDraggingTask(task);
|
|
26
27
|
e.dataTransfer.setData('taskId', task.id);
|
|
27
28
|
e.dataTransfer.setData('taskTitle', task.title || '');
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const toISO = (d) => {
|
|
30
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
31
|
+
return d.toString().split('T')[0];
|
|
32
|
+
if (d instanceof Date)
|
|
33
|
+
return d.toISOString().split('T')[0];
|
|
34
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
35
|
+
const parsed = new Date(d);
|
|
36
|
+
if (!isNaN(parsed.getTime()))
|
|
37
|
+
return parsed.toISOString().split('T')[0];
|
|
38
|
+
}
|
|
39
|
+
return String(d);
|
|
40
|
+
};
|
|
41
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
42
|
+
? task.dueDateStart
|
|
43
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
44
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate ? task.dueDateEnd : Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
45
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
46
|
+
? task.dueTimeStart
|
|
47
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
48
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
49
|
+
? task.dueTimeEnd
|
|
50
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
51
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
52
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
53
|
+
const estimatedTime = Number(dtEnd.since(dtStart).total('minute'));
|
|
31
54
|
e.dataTransfer.setData('taskEstimatedTime', String(estimatedTime));
|
|
32
55
|
if (task.color) {
|
|
33
56
|
e.dataTransfer.setData('taskColor', task.color);
|
|
@@ -51,10 +74,14 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
|
|
|
51
74
|
const overlappingTasksCount = tasks.filter((task) => {
|
|
52
75
|
if (draggingTask && task.id === draggingTask.id)
|
|
53
76
|
return false;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
77
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
78
|
+
? task.dueTimeStart
|
|
79
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
80
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
81
|
+
? task.dueTimeEnd
|
|
82
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
83
|
+
const taskStart = tStart.hour * 60 + tStart.minute;
|
|
84
|
+
const taskEnd = tEnd.hour * 60 + tEnd.minute;
|
|
58
85
|
return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
|
|
59
86
|
}).length;
|
|
60
87
|
return (_jsx(CalendarSlot, { slotIndex: slotIndex, onDrop: handleDrop, onCreateTask: onCreateTask, renderTask: renderTask, overlappingTasksCount: overlappingTasksCount, draggingTask: draggingTask || undefined, children: tasksForSlot?.map((task) => (_jsx(CalendarItemWrapper, { task: task, position: task.position, onDragStart: (e) => handleDragStart(e, task), onDragEnd: handleDragEnd, onResize: onTaskResize, renderTask: renderTask }, task.id))) }, slotIndex));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { Circle, CircleCheckBig } from 'lucide-react';
|
|
3
4
|
import { useState } from 'react';
|
|
4
5
|
import { CalendarLike, CalendarItem } from './calendar-like';
|
|
@@ -10,10 +11,11 @@ const meta = {
|
|
|
10
11
|
tags: ['autodocs'],
|
|
11
12
|
};
|
|
12
13
|
export default meta;
|
|
13
|
-
const getTodayAtHour = (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const getTodayAtHour = (_hour, _minutes = 0) => {
|
|
15
|
+
return Temporal.Now.plainDateISO();
|
|
16
|
+
};
|
|
17
|
+
const getTodayTimeAtHour = (hour, minutes = 0) => {
|
|
18
|
+
return Temporal.PlainTime.from({ hour, minute: minutes });
|
|
17
19
|
};
|
|
18
20
|
const INITIAL_TASKS = [
|
|
19
21
|
{
|
|
@@ -21,14 +23,18 @@ const INITIAL_TASKS = [
|
|
|
21
23
|
title: 'Утренняя почта',
|
|
22
24
|
description: 'Проверить входящие и ответить на важные письма',
|
|
23
25
|
dueDateStart: getTodayAtHour(9),
|
|
24
|
-
dueDateEnd: getTodayAtHour(9
|
|
26
|
+
dueDateEnd: getTodayAtHour(9),
|
|
27
|
+
dueTimeStart: getTodayTimeAtHour(9),
|
|
28
|
+
dueTimeEnd: getTodayTimeAtHour(9, 45),
|
|
25
29
|
color: '#3b82f6',
|
|
26
30
|
},
|
|
27
31
|
{
|
|
28
32
|
id: '2',
|
|
29
33
|
title: 'Стендап',
|
|
30
34
|
dueDateStart: getTodayAtHour(10),
|
|
31
|
-
dueDateEnd: getTodayAtHour(10
|
|
35
|
+
dueDateEnd: getTodayAtHour(10),
|
|
36
|
+
dueTimeStart: getTodayTimeAtHour(10),
|
|
37
|
+
dueTimeEnd: getTodayTimeAtHour(10, 15),
|
|
32
38
|
color: '#10b981',
|
|
33
39
|
isInWork: true,
|
|
34
40
|
},
|
|
@@ -36,7 +42,9 @@ const INITIAL_TASKS = [
|
|
|
36
42
|
id: '3',
|
|
37
43
|
title: 'Обед',
|
|
38
44
|
dueDateStart: getTodayAtHour(13),
|
|
39
|
-
dueDateEnd: getTodayAtHour(
|
|
45
|
+
dueDateEnd: getTodayAtHour(13),
|
|
46
|
+
dueTimeStart: getTodayTimeAtHour(13),
|
|
47
|
+
dueTimeEnd: getTodayTimeAtHour(14),
|
|
40
48
|
color: '#f59e0b',
|
|
41
49
|
},
|
|
42
50
|
{
|
|
@@ -44,14 +52,18 @@ const INITIAL_TASKS = [
|
|
|
44
52
|
title: 'Разработка фичи',
|
|
45
53
|
description: 'Реализация логики календаря',
|
|
46
54
|
dueDateStart: getTodayAtHour(14),
|
|
47
|
-
dueDateEnd: getTodayAtHour(
|
|
55
|
+
dueDateEnd: getTodayAtHour(14),
|
|
56
|
+
dueTimeStart: getTodayTimeAtHour(14),
|
|
57
|
+
dueTimeEnd: getTodayTimeAtHour(16),
|
|
48
58
|
color: '#8b5cf6',
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
id: '5',
|
|
52
62
|
title: 'Завершенная задача',
|
|
53
63
|
dueDateStart: getTodayAtHour(17),
|
|
54
|
-
dueDateEnd: getTodayAtHour(17
|
|
64
|
+
dueDateEnd: getTodayAtHour(17),
|
|
65
|
+
dueTimeStart: getTodayTimeAtHour(17),
|
|
66
|
+
dueTimeEnd: getTodayTimeAtHour(17, 30),
|
|
55
67
|
isCompleted: true,
|
|
56
68
|
color: '#8b5cf6',
|
|
57
69
|
},
|
|
@@ -65,11 +77,18 @@ export const Interactive = {
|
|
|
65
77
|
console.log(`Task ${taskId} dropped at ${hour}:${minutes}`);
|
|
66
78
|
setTasks((prev) => prev.map((t) => {
|
|
67
79
|
if (t.id === taskId) {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
80
|
+
const dtStart = t.dueDateStart.toPlainDateTime(t.dueTimeStart || Temporal.PlainTime.from('00:00'));
|
|
81
|
+
const dtEnd = (t.dueDateEnd || t.dueDateStart).toPlainDateTime(t.dueTimeEnd || Temporal.PlainTime.from('23:59'));
|
|
82
|
+
const duration = dtEnd.since(dtStart);
|
|
83
|
+
const newDtStart = t.dueDateStart.toPlainDateTime(Temporal.PlainTime.from({ hour, minute: minutes }));
|
|
84
|
+
const newDtEnd = newDtStart.add(duration);
|
|
85
|
+
return {
|
|
86
|
+
...t,
|
|
87
|
+
dueDateStart: newDtStart.toPlainDate(),
|
|
88
|
+
dueDateEnd: newDtEnd.toPlainDate(),
|
|
89
|
+
dueTimeStart: newDtStart.toPlainTime(),
|
|
90
|
+
dueTimeEnd: newDtEnd.toPlainTime(),
|
|
91
|
+
};
|
|
73
92
|
}
|
|
74
93
|
return t;
|
|
75
94
|
}));
|
|
@@ -78,25 +97,36 @@ export const Interactive = {
|
|
|
78
97
|
console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes. New start: ${newStartHour}:${newStartMinutes}`);
|
|
79
98
|
setTasks((prev) => prev.map((t) => {
|
|
80
99
|
if (t.id === taskId) {
|
|
81
|
-
|
|
100
|
+
let newDtStart;
|
|
82
101
|
if (newStartHour !== undefined && newStartMinutes !== undefined) {
|
|
83
|
-
|
|
102
|
+
newDtStart = t.dueDateStart.toPlainDateTime(Temporal.PlainTime.from({ hour: newStartHour, minute: newStartMinutes }));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
newDtStart = t.dueDateStart.toPlainDateTime(t.dueTimeStart || Temporal.PlainTime.from('00:00'));
|
|
84
106
|
}
|
|
85
|
-
const
|
|
86
|
-
return {
|
|
107
|
+
const newDtEnd = newDtStart.add({ minutes: newEstimatedTime });
|
|
108
|
+
return {
|
|
109
|
+
...t,
|
|
110
|
+
dueDateStart: newDtStart.toPlainDate(),
|
|
111
|
+
dueDateEnd: newDtEnd.toPlainDate(),
|
|
112
|
+
dueTimeStart: newDtStart.toPlainTime(),
|
|
113
|
+
dueTimeEnd: newDtEnd.toPlainTime(),
|
|
114
|
+
};
|
|
87
115
|
}
|
|
88
116
|
return t;
|
|
89
117
|
}));
|
|
90
118
|
};
|
|
91
119
|
const handleCreateTask = (hour, minutes, estimatedTime) => {
|
|
92
120
|
console.log(`Create task at ${hour}:${minutes} with duration ${estimatedTime}`);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
121
|
+
const dtStart = Temporal.Now.plainDateISO().toPlainDateTime(Temporal.PlainTime.from({ hour, minute: minutes }));
|
|
122
|
+
const dtEnd = dtStart.add({ minutes: estimatedTime });
|
|
95
123
|
const newTask = {
|
|
96
124
|
id: Math.random().toString(36).substr(2, 9),
|
|
97
125
|
title: 'Новая задача',
|
|
98
|
-
dueDateStart,
|
|
99
|
-
dueDateEnd,
|
|
126
|
+
dueDateStart: dtStart.toPlainDate(),
|
|
127
|
+
dueDateEnd: dtEnd.toPlainDate(),
|
|
128
|
+
dueTimeStart: dtStart.toPlainTime(),
|
|
129
|
+
dueTimeEnd: dtEnd.toPlainTime(),
|
|
100
130
|
color: '#8b5cf6',
|
|
101
131
|
};
|
|
102
132
|
setTasks((prev) => [...prev, newTask]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { animated, config, useSpring } from '@react-spring/web';
|
|
3
4
|
import { useState } from 'react';
|
|
4
5
|
import { jc } from '../../../utils';
|
|
@@ -13,7 +14,39 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
13
14
|
to: {
|
|
14
15
|
opacity: dragOverInfo ? 1 : 0.6,
|
|
15
16
|
height: dragOverInfo
|
|
16
|
-
?
|
|
17
|
+
? (() => {
|
|
18
|
+
const toISO = (d) => {
|
|
19
|
+
if (d instanceof Temporal.PlainDate ||
|
|
20
|
+
d instanceof Temporal.PlainDateTime ||
|
|
21
|
+
d instanceof Temporal.Instant)
|
|
22
|
+
return d.toString().split('T')[0];
|
|
23
|
+
if (d instanceof Date)
|
|
24
|
+
return d.toISOString().split('T')[0];
|
|
25
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
26
|
+
const parsed = new Date(d);
|
|
27
|
+
if (!isNaN(parsed.getTime()))
|
|
28
|
+
return parsed.toISOString().split('T')[0];
|
|
29
|
+
}
|
|
30
|
+
return String(d);
|
|
31
|
+
};
|
|
32
|
+
const task = dragOverInfo.task;
|
|
33
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
34
|
+
? task.dueDateStart
|
|
35
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
36
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate
|
|
37
|
+
? task.dueDateEnd
|
|
38
|
+
: Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
39
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
40
|
+
? task.dueTimeStart
|
|
41
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
42
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
43
|
+
? task.dueTimeEnd
|
|
44
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
45
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
46
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
47
|
+
const minutes = Number(dtEnd.since(dtStart).total('minute'));
|
|
48
|
+
return `${(minutes / 15) * 20}px`;
|
|
49
|
+
})()
|
|
17
50
|
: '40px',
|
|
18
51
|
},
|
|
19
52
|
config: { ...config.stiff, precision: 0.001 },
|
|
@@ -83,8 +116,12 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
83
116
|
}, children: renderTask ? (renderTask({
|
|
84
117
|
id: 'new-task-preview',
|
|
85
118
|
title: 'Новая задача',
|
|
86
|
-
dueDateStart:
|
|
87
|
-
|
|
119
|
+
dueDateStart: Temporal.Now.plainDateISO(),
|
|
120
|
+
dueTimeStart: Temporal.PlainTime.from({ hour, minute: minutes }),
|
|
121
|
+
dueDateEnd: Temporal.Now.plainDateISO(),
|
|
122
|
+
dueTimeEnd: Temporal.PlainTime.from({ hour, minute: minutes }).add({
|
|
123
|
+
minutes: selection.currentEstimatedTime,
|
|
124
|
+
}),
|
|
88
125
|
color: 'white',
|
|
89
126
|
})) : (_jsxs("div", { className: "border rounded-sm h-full p-1", children: [_jsx(Typo, { size: "xs", weight: "500", children: "\u041D\u043E\u0432\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430" }), _jsxs(Typo, { size: "xs", color: "secondary", children: [selection.currentEstimatedTime, " \u043C\u0438\u043D"] })] })) }))] })] }));
|
|
90
127
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { ReactNode } from 'react';
|
|
2
3
|
export type TCalendarTask = {
|
|
3
4
|
id: string;
|
|
4
|
-
dueDateStart:
|
|
5
|
-
dueDateEnd:
|
|
5
|
+
dueDateStart: Temporal.PlainDate;
|
|
6
|
+
dueDateEnd: Temporal.PlainDate;
|
|
6
7
|
[key: string]: any;
|
|
7
8
|
};
|
|
8
9
|
export type TCalendarTaskWithPosition = TCalendarTask & {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { useEffect } from 'react';
|
|
2
3
|
export const useAutoScroll = (containerRef, enabled, topOffset) => {
|
|
3
4
|
useEffect(() => {
|
|
@@ -5,9 +6,9 @@ export const useAutoScroll = (containerRef, enabled, topOffset) => {
|
|
|
5
6
|
const scroll = (retryCount = 0) => {
|
|
6
7
|
if (!containerRef.current)
|
|
7
8
|
return;
|
|
8
|
-
const now =
|
|
9
|
-
const hours = now.
|
|
10
|
-
const minutes = now.
|
|
9
|
+
const now = Temporal.Now.plainTimeISO();
|
|
10
|
+
const hours = now.hour;
|
|
11
|
+
const minutes = now.minute;
|
|
11
12
|
const totalMinutes = hours * 60 + minutes;
|
|
12
13
|
// Находим ближайший скроллируемый родитель или используем окно
|
|
13
14
|
const scrollParent = (node) => {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { useState, useEffect, useMemo } from 'react';
|
|
2
3
|
export const useCurrentTime = (updateInterval = 60000) => {
|
|
3
|
-
const [now, setNow] = useState(
|
|
4
|
+
const [now, setNow] = useState(Temporal.Now.plainTimeISO());
|
|
4
5
|
useEffect(() => {
|
|
5
6
|
const interval = setInterval(() => {
|
|
6
|
-
setNow(
|
|
7
|
+
setNow(Temporal.Now.plainTimeISO());
|
|
7
8
|
}, updateInterval);
|
|
8
9
|
return () => clearInterval(interval);
|
|
9
10
|
}, [updateInterval]);
|
|
10
11
|
const topOffset = useMemo(() => {
|
|
11
|
-
const hours = now.
|
|
12
|
-
const minutes = now.
|
|
12
|
+
const hours = now.hour;
|
|
13
|
+
const minutes = now.minute;
|
|
13
14
|
const totalMinutes = hours * 60 + minutes;
|
|
14
15
|
// 15 минут = 20 пикселей (высота слота)
|
|
15
16
|
return totalMinutes * (20 / 15);
|
|
@@ -1,24 +1,39 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
const toDateTime = (date, time) => {
|
|
3
|
+
const toISO = (d) => {
|
|
4
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
5
|
+
return d.toString().split('T')[0];
|
|
6
|
+
if (d instanceof Date)
|
|
7
|
+
return d.toISOString().split('T')[0];
|
|
8
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
9
|
+
const parsed = new Date(d);
|
|
10
|
+
if (!isNaN(parsed.getTime()))
|
|
11
|
+
return parsed.toISOString().split('T')[0];
|
|
12
|
+
}
|
|
13
|
+
return String(d);
|
|
14
|
+
};
|
|
15
|
+
const d = date instanceof Temporal.PlainDate ? date : Temporal.PlainDate.from(toISO(date));
|
|
16
|
+
const t = time instanceof Temporal.PlainTime ? time : Temporal.PlainTime.from(String(time || '00:00'));
|
|
17
|
+
return d.toPlainDateTime(t);
|
|
18
|
+
};
|
|
1
19
|
export const calculateTaskPositions = (tasks) => {
|
|
2
20
|
const tasksToRender = [...tasks]
|
|
3
21
|
.map((t) => {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
22
|
+
const dtStart = toDateTime(t.dueDateStart, t.dueTimeStart);
|
|
23
|
+
const dtEnd = toDateTime(t.dueDateEnd, t.dueTimeEnd);
|
|
6
24
|
return {
|
|
7
25
|
...t,
|
|
8
|
-
|
|
9
|
-
|
|
26
|
+
dtStart,
|
|
27
|
+
dtEnd,
|
|
10
28
|
};
|
|
11
29
|
})
|
|
12
|
-
.
|
|
13
|
-
.sort((a, b) => a.dueDateStart.getTime() - b.dueDateStart.getTime());
|
|
30
|
+
.sort((a, b) => Temporal.PlainDateTime.compare(a.dtStart, b.dtStart));
|
|
14
31
|
const columns = [];
|
|
15
32
|
tasksToRender.forEach((task) => {
|
|
16
|
-
const startTime = task.dueDateStart.getTime();
|
|
17
33
|
let placed = false;
|
|
18
34
|
for (const column of columns) {
|
|
19
35
|
const lastTask = column[column.length - 1];
|
|
20
|
-
|
|
21
|
-
if (startTime >= lastEndTime) {
|
|
36
|
+
if (Temporal.PlainDateTime.compare(task.dtStart, lastTask.dtEnd) >= 0) {
|
|
22
37
|
column.push(task);
|
|
23
38
|
placed = true;
|
|
24
39
|
break;
|
|
@@ -30,24 +45,27 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
30
45
|
});
|
|
31
46
|
const clusters = [];
|
|
32
47
|
let currentCluster = [];
|
|
33
|
-
let clusterMaxEndTime =
|
|
48
|
+
let clusterMaxEndTime = null;
|
|
34
49
|
columns.forEach((column) => {
|
|
35
|
-
let columnMinStartTime =
|
|
50
|
+
let columnMinStartTime = null;
|
|
36
51
|
column.forEach((t) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
if (!columnMinStartTime || Temporal.PlainDateTime.compare(t.dtStart, columnMinStartTime) < 0) {
|
|
53
|
+
columnMinStartTime = t.dtStart;
|
|
54
|
+
}
|
|
40
55
|
});
|
|
41
|
-
if (
|
|
56
|
+
if (clusterMaxEndTime &&
|
|
57
|
+
columnMinStartTime &&
|
|
58
|
+
Temporal.PlainDateTime.compare(columnMinStartTime, clusterMaxEndTime) >= 0 &&
|
|
59
|
+
currentCluster.length > 0) {
|
|
42
60
|
clusters.push(currentCluster);
|
|
43
61
|
currentCluster = [];
|
|
44
|
-
clusterMaxEndTime =
|
|
62
|
+
clusterMaxEndTime = null;
|
|
45
63
|
}
|
|
46
64
|
currentCluster.push(column);
|
|
47
65
|
column.forEach((t) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
if (!clusterMaxEndTime || Temporal.PlainDateTime.compare(t.dtEnd, clusterMaxEndTime) > 0) {
|
|
67
|
+
clusterMaxEndTime = t.dtEnd;
|
|
68
|
+
}
|
|
51
69
|
});
|
|
52
70
|
});
|
|
53
71
|
if (currentCluster.length > 0)
|
|
@@ -60,11 +78,11 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
60
78
|
const width = clusterColumnsCount > 1 ? `${100 / clusterColumnsCount}%` : '100%';
|
|
61
79
|
const left = clusterColumnsCount > 1 ? `${(colIndex * 100) / clusterColumnsCount}%` : '0%';
|
|
62
80
|
// Расчитываем смещение внутри 15-минутного слота
|
|
63
|
-
const minutes = task.
|
|
81
|
+
const minutes = task.dtStart.minute;
|
|
64
82
|
const minutesInSlot = minutes % 15;
|
|
65
83
|
const topOffset = (minutesInSlot / 15) * 20;
|
|
66
84
|
const top = `${Math.round(topOffset)}px`;
|
|
67
|
-
const diffMinutes = Math.round((task.
|
|
85
|
+
const diffMinutes = Math.round(Number(task.dtEnd.since(task.dtStart).total('minute')));
|
|
68
86
|
const height = `${Math.round((Math.max(15, diffMinutes || 15) / 15) * 20)}px`;
|
|
69
87
|
result.push({
|
|
70
88
|
...task,
|
|
@@ -78,9 +96,9 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
78
96
|
export const groupTasksBySlot = (tasksWithPosition) => {
|
|
79
97
|
const map = {};
|
|
80
98
|
tasksWithPosition.forEach((task) => {
|
|
81
|
-
const
|
|
82
|
-
const hour =
|
|
83
|
-
const minutes =
|
|
99
|
+
const dtStart = toDateTime(task.dueDateStart, task.dueTimeStart);
|
|
100
|
+
const hour = dtStart.hour;
|
|
101
|
+
const minutes = dtStart.minute;
|
|
84
102
|
const slotIndex = hour * 4 + Math.floor(minutes / 15);
|
|
85
103
|
if (!map[slotIndex])
|
|
86
104
|
map[slotIndex] = [];
|
package/package.json
CHANGED
|
@@ -1,69 +1,77 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kkkarsss/ui",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "UI Kit for kkkarsss projects",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
],
|
|
11
|
-
"scripts": {
|
|
12
|
-
"build": "tsc && copy src\\index.css dist\\index.css",
|
|
13
|
-
"prepare": "npm run build",
|
|
14
|
-
"lint": "eslint .",
|
|
15
|
-
"format": "prettier --write .",
|
|
16
|
-
"storybook": "storybook dev -p 6006",
|
|
17
|
-
"build-storybook": "storybook build"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [],
|
|
20
|
-
"author": "",
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"peerDependencies": {
|
|
23
|
-
"react": ">=18",
|
|
24
|
-
"react-dom": ">=18"
|
|
25
|
-
},
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"@chromatic-com/storybook": "^5.0.0",
|
|
28
|
-
"@eslint/js": "^9.39.2",
|
|
29
|
-
"@storybook/addon-a11y": "^10.2.7",
|
|
30
|
-
"@storybook/addon-docs": "^10.2.7",
|
|
31
|
-
"@storybook/addon-onboarding": "^10.2.7",
|
|
32
|
-
"@storybook/addon-vitest": "^10.2.7",
|
|
33
|
-
"@storybook/react": "^10.2.7",
|
|
34
|
-
"@storybook/react-vite": "^10.2.7",
|
|
35
|
-
"@tailwindcss/container-queries": "^0.1.1",
|
|
36
|
-
"
|
|
37
|
-
"@types/
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
-
"@
|
|
40
|
-
"@vitest/
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"eslint": "^
|
|
44
|
-
"eslint-
|
|
45
|
-
"eslint-import
|
|
46
|
-
"eslint-plugin-
|
|
47
|
-
"eslint-plugin-react-
|
|
48
|
-
"eslint-plugin-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"react": "^19.2.3",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"typescript": "5.9.3",
|
|
59
|
-
"typescript-eslint": "^8.53.1",
|
|
60
|
-
"vite": "^7.3.1",
|
|
61
|
-
"vitest": "^4.0.18"
|
|
62
|
-
},
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"@
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"react-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@kkkarsss/ui",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "UI Kit for kkkarsss projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && copy src\\index.css dist\\index.css",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"storybook": "storybook dev -p 6006",
|
|
17
|
+
"build-storybook": "storybook build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18",
|
|
24
|
+
"react-dom": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@chromatic-com/storybook": "^5.0.0",
|
|
28
|
+
"@eslint/js": "^9.39.2",
|
|
29
|
+
"@storybook/addon-a11y": "^10.2.7",
|
|
30
|
+
"@storybook/addon-docs": "^10.2.7",
|
|
31
|
+
"@storybook/addon-onboarding": "^10.2.7",
|
|
32
|
+
"@storybook/addon-vitest": "^10.2.7",
|
|
33
|
+
"@storybook/react": "^10.2.7",
|
|
34
|
+
"@storybook/react-vite": "^10.2.7",
|
|
35
|
+
"@tailwindcss/container-queries": "^0.1.1",
|
|
36
|
+
"@types/node": "^25.0.10",
|
|
37
|
+
"@types/react": "^19.2.9",
|
|
38
|
+
"@types/react-dom": "^19.2.3",
|
|
39
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
40
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
41
|
+
"autoprefixer": "^10.4.23",
|
|
42
|
+
"eslint": "^9.39.1",
|
|
43
|
+
"eslint-config-prettier": "^10.1.8",
|
|
44
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
45
|
+
"eslint-plugin-import": "^2.32.0",
|
|
46
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
47
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
48
|
+
"eslint-plugin-storybook": "^10.2.7",
|
|
49
|
+
"globals": "^16.5.0",
|
|
50
|
+
"lucide-react": "^0.563.0",
|
|
51
|
+
"playwright": "^1.58.0",
|
|
52
|
+
"postcss": "^8.5.6",
|
|
53
|
+
"prettier": "^3.8.1",
|
|
54
|
+
"react": "^19.2.3",
|
|
55
|
+
"react-dom": "^19.2.3",
|
|
56
|
+
"storybook": "^10.2.7",
|
|
57
|
+
"tailwindcss": "^3.4.19",
|
|
58
|
+
"typescript": "5.9.3",
|
|
59
|
+
"typescript-eslint": "^8.53.1",
|
|
60
|
+
"vite": "^7.3.1",
|
|
61
|
+
"vitest": "^4.0.18"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@js-temporal/polyfill": "^0.5.1",
|
|
65
|
+
"@react-spring/web": "^10.0.3",
|
|
66
|
+
"date-fns": "^4.1.0",
|
|
67
|
+
"react-datepicker": "^9.1.0",
|
|
68
|
+
"react-day-picker": "^9.13.0",
|
|
69
|
+
"clsx": "^2.1.1",
|
|
70
|
+
"tailwind-merge": "^2.5.2",
|
|
71
|
+
"class-variance-authority": "^0.7.0",
|
|
72
|
+
"radix-ui": "^1.1.0",
|
|
73
|
+
"@radix-ui/react-popover": "^1.1.2",
|
|
74
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
75
|
+
"tailwindcss-animate": "^1.0.7"
|
|
76
|
+
}
|
|
77
|
+
}
|