@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.
Files changed (27) hide show
  1. package/dist/components/ui/button.d.ts +16 -0
  2. package/dist/components/ui/button.js +33 -0
  3. package/dist/components/ui/calendar.d.ts +4 -0
  4. package/dist/components/ui/calendar.js +41 -0
  5. package/dist/components/ui/popover.d.ts +6 -0
  6. package/dist/components/ui/popover.js +9 -0
  7. package/dist/index.css +104 -47
  8. package/dist/lib/utils.d.ts +2 -0
  9. package/dist/lib/utils.js +5 -0
  10. package/dist/ui/controls/date-input/date-input.d.ts +1 -0
  11. package/dist/ui/controls/date-input/date-input.js +33 -16
  12. package/dist/ui/controls/date-input/date-input.stories.d.ts +10 -0
  13. package/dist/ui/controls/date-input/date-input.stories.js +56 -0
  14. package/dist/ui/controls/date-picker/date-picker.d.ts +7 -2
  15. package/dist/ui/controls/date-picker/date-picker.js +6 -30
  16. package/dist/ui/controls/date-picker/date-picker.stories.d.ts +8 -0
  17. package/dist/ui/controls/date-picker/date-picker.stories.js +46 -0
  18. package/dist/ui/information/calendar-like/calendar-item-wrapper.js +34 -7
  19. package/dist/ui/information/calendar-like/calendar-like.js +34 -7
  20. package/dist/ui/information/calendar-like/calendar-like.stories.js +52 -22
  21. package/dist/ui/information/calendar-like/calendar-slot.js +40 -3
  22. package/dist/ui/information/calendar-like/types.d.ts +3 -2
  23. package/dist/ui/information/calendar-like/use-auto-scroll.js +4 -3
  24. package/dist/ui/information/calendar-like/use-current-time.d.ts +2 -1
  25. package/dist/ui/information/calendar-like/use-current-time.js +5 -4
  26. package/dist/ui/information/calendar-like/utils.js +42 -24
  27. 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,4 @@
1
+ import * as React from 'react';
2
+ import { DayPicker } from 'react-day-picker';
3
+ declare function Calendar({ className, classNames, showOutsideDays, ...props }: React.ComponentProps<typeof DayPicker>): import("react/jsx-runtime").JSX.Element;
4
+ export { Calendar };
@@ -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
- :root {
138
- font-family: Roboto, sans-serif;
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
- /* Dark theme (default) */
148
- --background: #232323;
149
- --background-accent: #272727;
150
- --foreground: #d9d9d9;
215
+ .light {
216
+ --background: 0 0% 96%;
217
+ --foreground: 240 10% 4%;
151
218
 
152
- --secondary: #3b3b3b;
153
- --secondary-foreground: #999;
219
+ --card: 0 0% 100%;
220
+ --card-foreground: 222.2 84% 4.9%;
154
221
 
155
- --accent: #ed3a64;
156
- --accent-foreground: #ffffff;
157
- --shadow: rgba(0, 0, 0, 0.1);
222
+ --popover: 0 0% 100%;
223
+ --popover-foreground: 222.2 84% 4.9%;
158
224
 
159
- --success: #22c55e;
160
- --warning: #f59e0b;
161
- --error: #ef4444;
225
+ --primary: 222.2 47.4% 11.2%;
226
+ --primary-foreground: 210 40% 98%;
162
227
 
163
- font-synthesis: none;
164
- text-rendering: optimizeLegibility;
165
- -webkit-font-smoothing: antialiased;
166
- }
228
+ --secondary: 220 14% 91%;
229
+ --secondary-foreground: 240 0% 57%;
167
230
 
168
- .dark {
169
- color-scheme: dark;
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
- --secondary: #3b3b3b;
175
- --secondary-foreground: #999;
234
+ --accent: 346 84% 58%;
235
+ --accent-foreground: 0 0% 100%;
176
236
 
177
- --accent: #ed3a64;
178
- --accent-foreground: #ffffff;
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
- .light {
183
- color-scheme: light;
184
- --background: #f4f4f4;
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
- --secondary: #e5e7eb;
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
 
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -6,5 +6,6 @@ export type TDateInputProps = {
6
6
  placeholder?: string;
7
7
  size?: 's' | 'm' | 'l';
8
8
  disabled?: boolean;
9
+ showTime?: boolean;
9
10
  };
10
11
  export declare const DateInput: FC<TDateInputProps>;
@@ -1,25 +1,42 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Calendar } from 'lucide-react';
3
- import { useRef, useState } from 'react';
4
- import { Dropdown } from '../../layout';
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 year = date.getFullYear();
13
- const month = String(date.getMonth() + 1).padStart(2, '0');
14
- const day = String(date.getDate()).padStart(2, '0');
15
- onChange(`${year}-${month}-${day}`);
16
- setIsOpen(false);
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 selectedDate = value ? new Date(value) : undefined;
20
- return (_jsxs("div", { ref: containerRef, className: "relative w-full", children: [_jsx(Input, { label: label, value: value, onChange: (e) => onChange(e.target.value), type: "text", placeholder: placeholder || 'YYYY-MM-DD', size: size, disabled: disabled, rightAcc: _jsx("div", { className: "flex items-center h-full", children: _jsx(Calendar, { size: 16, className: "cursor-pointer text-secondary transition-colors hover:text-accent", onClick: (e) => {
21
- e.preventDefault();
22
- e.stopPropagation();
23
- setIsOpen(!isOpen);
24
- } }) }) }), _jsx(Dropdown, { isOpen: isOpen, matchWidth: false, onClose: () => setIsOpen(false), anchorRef: containerRef, children: _jsx(DatePicker, { selected: selectedDate, onSelect: handleDateSelect }) })] }));
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/dist/style.css';
2
+ import type { DateRange } from 'react-day-picker';
3
3
  export type TDatePickerProps = {
4
+ mode?: 'single';
4
5
  selected?: Date;
5
- onSelect: (date: Date | undefined) => void;
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 { DayPicker } from 'react-day-picker';
3
- import 'react-day-picker/dist/style.css';
4
- export const DatePicker = ({ selected, onSelect }) => {
5
- return (_jsx("div", { className: `rounded-lg p-4 shadow-md`, style: {
6
- background: `var(--background-accent)`,
7
- color: `var(--foreground)`,
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 dStart = task.dueDateStart instanceof Date ? task.dueDateStart : new Date(task.dueDateStart);
12
- const dEnd = task.dueDateEnd instanceof Date ? task.dueDateEnd : new Date(task.dueDateEnd);
13
- const estimatedTime = (dEnd.getTime() - dStart.getTime()) / (60 * 1000);
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 = dStart.getHours();
16
- const startMinutes = dStart.getMinutes();
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 dStart = task.dueDateStart instanceof Date ? task.dueDateStart : new Date(task.dueDateStart);
78
- const originalStartTotalMinutes = dStart.getHours() * 60 + dStart.getMinutes();
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 dStart = task.dueDateStart instanceof Date ? task.dueDateStart : new Date(task.dueDateStart);
29
- const dEnd = task.dueDateEnd instanceof Date ? task.dueDateEnd : new Date(task.dueDateEnd);
30
- const estimatedTime = (dEnd.getTime() - dStart.getTime()) / (60 * 1000);
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 dStart = task.dueDateStart instanceof Date ? task.dueDateStart : new Date(task.dueDateStart);
55
- const dEnd = task.dueDateEnd instanceof Date ? task.dueDateEnd : new Date(task.dueDateEnd);
56
- const taskStart = dStart.getHours() * 60 + dStart.getMinutes();
57
- const taskEnd = dEnd.getHours() * 60 + dEnd.getMinutes();
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 = (hour, minutes = 0) => {
14
- const date = new Date();
15
- date.setHours(hour, minutes, 0, 0);
16
- return date;
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, 45),
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, 15),
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(14),
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(16),
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, 30),
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 durationMs = t.dueDateEnd.getTime() - t.dueDateStart.getTime();
69
- const newStartDate = new Date(t.dueDateStart);
70
- newStartDate.setHours(hour, minutes, 0, 0);
71
- const newEndDate = new Date(newStartDate.getTime() + durationMs);
72
- return { ...t, dueDateStart: newStartDate, dueDateEnd: newEndDate };
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
- const newStartDate = new Date(t.dueDateStart);
100
+ let newDtStart;
82
101
  if (newStartHour !== undefined && newStartMinutes !== undefined) {
83
- newStartDate.setHours(newStartHour, newStartMinutes, 0, 0);
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 newEndDate = new Date(newStartDate.getTime() + newEstimatedTime * 60 * 1000);
86
- return { ...t, dueDateStart: newStartDate, dueDateEnd: newEndDate };
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 dueDateStart = getTodayAtHour(hour, minutes);
94
- const dueDateEnd = new Date(dueDateStart.getTime() + estimatedTime * 60 * 1000);
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
- ? `${((new Date(dragOverInfo.task.dueDateEnd).getTime() - new Date(dragOverInfo.task.dueDateStart).getTime()) / (60 * 1000) / 15) * 20}px`
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: new Date(new Date().setHours(hour, minutes, 0, 0)),
87
- dueDateEnd: new Date(new Date().setHours(hour, minutes + selection.currentEstimatedTime, 0, 0)),
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: Date;
5
- dueDateEnd: Date;
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 = new Date();
9
- const hours = now.getHours();
10
- const minutes = now.getMinutes();
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,4 +1,5 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
1
2
  export declare const useCurrentTime: (updateInterval?: number) => {
2
- now: Date;
3
+ now: Temporal.PlainTime;
3
4
  topOffset: number;
4
5
  };
@@ -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(new Date());
4
+ const [now, setNow] = useState(Temporal.Now.plainTimeISO());
4
5
  useEffect(() => {
5
6
  const interval = setInterval(() => {
6
- setNow(new Date());
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.getHours();
12
- const minutes = now.getMinutes();
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 dStart = t.dueDateStart instanceof Date ? t.dueDateStart : new Date(t.dueDateStart);
5
- const dEnd = t.dueDateEnd instanceof Date ? t.dueDateEnd : new Date(t.dueDateEnd);
22
+ const dtStart = toDateTime(t.dueDateStart, t.dueTimeStart);
23
+ const dtEnd = toDateTime(t.dueDateEnd, t.dueTimeEnd);
6
24
  return {
7
25
  ...t,
8
- dueDateStart: dStart,
9
- dueDateEnd: dEnd,
26
+ dtStart,
27
+ dtEnd,
10
28
  };
11
29
  })
12
- .filter((t) => t.dueDateStart && !isNaN(t.dueDateStart.getTime()) && t.dueDateEnd && !isNaN(t.dueDateEnd.getTime()))
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
- const lastEndTime = lastTask.dueDateEnd.getTime();
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 = 0;
48
+ let clusterMaxEndTime = null;
34
49
  columns.forEach((column) => {
35
- let columnMinStartTime = Infinity;
50
+ let columnMinStartTime = null;
36
51
  column.forEach((t) => {
37
- const start = t.dueDateStart.getTime();
38
- if (start < columnMinStartTime)
39
- columnMinStartTime = start;
52
+ if (!columnMinStartTime || Temporal.PlainDateTime.compare(t.dtStart, columnMinStartTime) < 0) {
53
+ columnMinStartTime = t.dtStart;
54
+ }
40
55
  });
41
- if (columnMinStartTime >= clusterMaxEndTime && currentCluster.length > 0) {
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 = 0;
62
+ clusterMaxEndTime = null;
45
63
  }
46
64
  currentCluster.push(column);
47
65
  column.forEach((t) => {
48
- const end = t.dueDateEnd.getTime();
49
- if (end > clusterMaxEndTime)
50
- clusterMaxEndTime = end;
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.dueDateStart.getMinutes();
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.dueDateEnd.getTime() - task.dueDateStart.getTime()) / (60 * 1000));
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 dStart = task.dueDateStart instanceof Date ? task.dueDateStart : new Date(task.dueDateStart);
82
- const hour = dStart.getHours();
83
- const minutes = dStart.getMinutes();
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": "1.5.5",
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
- "tailwindcss": "^3.4.19",
37
- "@types/node": "^25.0.10",
38
- "@types/react": "^19.2.9",
39
- "@types/react-dom": "^19.2.3",
40
- "@vitest/browser-playwright": "^4.0.18",
41
- "@vitest/coverage-v8": "^4.0.18",
42
- "autoprefixer": "^10.4.23",
43
- "eslint": "^9.39.1",
44
- "eslint-config-prettier": "^10.1.8",
45
- "eslint-import-resolver-typescript": "^4.4.4",
46
- "eslint-plugin-import": "^2.32.0",
47
- "eslint-plugin-react-hooks": "^7.0.1",
48
- "eslint-plugin-react-refresh": "^0.4.26",
49
- "eslint-plugin-storybook": "^10.2.7",
50
- "globals": "^16.5.0",
51
- "lucide-react": "^0.563.0",
52
- "playwright": "^1.58.0",
53
- "postcss": "^8.5.6",
54
- "prettier": "^3.8.1",
55
- "react": "^19.2.3",
56
- "react-dom": "^19.2.3",
57
- "storybook": "^10.2.7",
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
- "@react-spring/web": "^10.0.3",
65
- "date-fns": "^4.1.0",
66
- "react-datepicker": "^9.1.0",
67
- "react-day-picker": "^9.13.0"
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
+ }