@kkkarsss/ui 1.4.9 → 1.4.11

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.
@@ -10,7 +10,7 @@ export type TCalendarItemWrapperProps = {
10
10
  };
11
11
  onDragStart?: (e: DragEvent<HTMLDivElement>, id: string) => void;
12
12
  onDragEnd?: (e: DragEvent<HTMLDivElement>, id: string) => void;
13
- onResize?: (id: string, newEstimatedTime: number) => void;
13
+ onResize?: (id: string, newEstimatedTime: number, newStartHour?: number, newStartMinutes?: number) => void;
14
14
  renderTask: (task: TCalendarTask) => ReactNode;
15
15
  overlappingTasksCount?: number;
16
16
  };
@@ -2,28 +2,60 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { jc } from '../../../utils';
4
4
  export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, onResize, renderTask, }) => {
5
- const [previewEstimatedTime, setPreviewEstimatedTime] = useState(null);
6
- const handleMouseDown = (e) => {
5
+ const [preview, setPreview] = useState(null);
6
+ const handleResize = (e, type) => {
7
7
  if (onResize) {
8
8
  e.preventDefault();
9
9
  e.stopPropagation();
10
10
  const startY = e.clientY;
11
11
  const startHeight = (task.estimatedTime / 15) * 20;
12
+ const startHour = task.dueDate.getHours();
13
+ const startMinutes = task.dueDate.getMinutes();
12
14
  const onMouseMove = (moveEvent) => {
13
15
  const deltaY = moveEvent.clientY - startY;
14
- const newHeight = Math.max(20, startHeight + deltaY);
15
- // Округляем до 15 минут (20px)
16
- const newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
17
- setPreviewEstimatedTime(newEstimatedTime);
16
+ let newHeight;
17
+ let newEstimatedTime;
18
+ let newStartHour = startHour;
19
+ let newStartMinutes = startMinutes;
20
+ if (type === 'bottom') {
21
+ newHeight = Math.max(20, startHeight + deltaY);
22
+ newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
23
+ }
24
+ else {
25
+ // Resizing top
26
+ const deltaMinutes = Math.round(deltaY / 20) * 15;
27
+ newEstimatedTime = Math.max(15, task.estimatedTime - deltaMinutes);
28
+ // If we hit min height, don't move the top further down
29
+ const actualDeltaMinutes = task.estimatedTime - newEstimatedTime;
30
+ const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
31
+ newStartHour = Math.floor(totalStartMinutes / 60);
32
+ newStartMinutes = totalStartMinutes % 60;
33
+ }
34
+ setPreview({ estimatedTime: newEstimatedTime, startHour: newStartHour, startMinutes: newStartMinutes });
18
35
  };
19
36
  const onMouseUp = (upEvent) => {
20
37
  const deltaY = upEvent.clientY - startY;
21
- const newHeight = Math.max(20, startHeight + deltaY);
22
- const newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
23
- if (newEstimatedTime !== task.estimatedTime) {
24
- onResize(task.id, newEstimatedTime);
38
+ let finalEstimatedTime;
39
+ let finalStartHour = startHour;
40
+ let finalStartMinutes = startMinutes;
41
+ if (type === 'bottom') {
42
+ const finalHeight = Math.max(20, startHeight + deltaY);
43
+ finalEstimatedTime = Math.max(15, Math.round(finalHeight / 20) * 15);
44
+ }
45
+ else {
46
+ const deltaMinutes = Math.round(deltaY / 20) * 15;
47
+ finalEstimatedTime = Math.max(15, task.estimatedTime - deltaMinutes);
48
+ const actualDeltaMinutes = task.estimatedTime - finalEstimatedTime;
49
+ const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
50
+ finalStartHour = Math.floor(totalStartMinutes / 60);
51
+ finalStartMinutes = totalStartMinutes % 60;
52
+ }
53
+ if (finalEstimatedTime !== task.estimatedTime ||
54
+ finalStartHour !== startHour ||
55
+ finalStartMinutes !== startMinutes) {
56
+ onResize(task.id, finalEstimatedTime, finalStartHour, finalStartMinutes);
25
57
  }
26
- setPreviewEstimatedTime(null);
58
+ setPreview(null);
27
59
  document.removeEventListener('mousemove', onMouseMove);
28
60
  document.removeEventListener('mouseup', onMouseUp);
29
61
  const clickPreventer = (e) => {
@@ -36,9 +68,18 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
36
68
  document.addEventListener('mouseup', onMouseUp);
37
69
  }
38
70
  };
39
- const displayHeight = previewEstimatedTime ? `${(previewEstimatedTime / 15) * 20}px` : position.height;
71
+ const displayHeight = preview ? `${(preview.estimatedTime / 15) * 20}px` : position.height;
72
+ let displayTop = position.top;
73
+ if (preview && (preview.startHour !== undefined || preview.startMinutes !== undefined)) {
74
+ const originalStartTotalMinutes = task.dueDate.getHours() * 60 + task.dueDate.getMinutes();
75
+ const newStartTotalMinutes = preview.startHour * 60 + preview.startMinutes;
76
+ const diffMinutes = newStartTotalMinutes - originalStartTotalMinutes;
77
+ const diffPixels = (diffMinutes / 15) * 20;
78
+ // Исходный position.top уже включает minutes % 15
79
+ const originalTopPx = parseInt(position.top, 10);
80
+ displayTop = `${originalTopPx + diffPixels}px`;
81
+ }
40
82
  return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
41
- console.log('CalendarItemWrapper: onDragStart', task.id);
42
83
  onDragStart?.(e, task.id);
43
84
  const el = e.currentTarget;
44
85
  // Создаем пустой элемент для скрытия стандартного ghost image браузера
@@ -53,7 +94,6 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
53
94
  }, 0);
54
95
  }
55
96
  }, onDragEnd: (e) => {
56
- console.log('CalendarItemWrapper: onDragEnd');
57
97
  onDragEnd?.(e, task.id);
58
98
  const el = e.currentTarget;
59
99
  if (el) {
@@ -64,6 +104,6 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
64
104
  height: displayHeight,
65
105
  width: position.width,
66
106
  left: position.left,
67
- top: position.top,
68
- }, className: jc('calendar-item absolute p-[2px] transition-[height,width,left,opacity] flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]', previewEstimatedTime !== null ? 'transition-none z-[60]' : '', 'active:z-[100]'), children: _jsxs("div", { className: "w-full h-full relative group/item", children: [renderTask(task), onResize && (_jsx("div", { className: "absolute bottom-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center z-[70]", onMouseDown: handleMouseDown, children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) }))] }) }));
107
+ top: displayTop,
108
+ }, className: jc('calendar-item absolute p-[2px] transition-[height,width,left,top,opacity] flex flex-col z-50 select-none', onDragStart ? 'cursor-move' : '', 'hover:z-[60]', preview !== null ? 'transition-none z-[60]' : '', 'active:z-[100]'), children: _jsxs("div", { className: "w-full h-full relative group/item", children: [onResize && (_jsx("div", { className: "absolute top-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center z-[70]", onMouseDown: (e) => handleResize(e, 'top'), children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) })), renderTask(task), onResize && (_jsx("div", { className: "absolute bottom-0 left-0 right-1 h-2 cursor-ns-resize group/resize flex items-center justify-center z-[70]", onMouseDown: (e) => handleResize(e, 'bottom'), children: _jsx("div", { className: "w-8 h-1 bg-accent/30 rounded-full opacity-0 group-hover/resize:opacity-100 transition-opacity" }) }))] }) }));
69
109
  };
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState, useEffect, useRef } from 'react';
2
+ import { useMemo, useState, useRef } from 'react';
3
3
  import { CalendarItemWrapper } from './calendar-item-wrapper';
4
4
  import { CalendarSlot } from './calendar-slot';
5
+ import { useAutoScroll } from './use-auto-scroll';
6
+ import { useCurrentTime } from './use-current-time';
5
7
  import { calculateTaskPositions, groupTasksBySlot } from './utils';
6
8
  import { jc } from '../../../utils';
7
9
  export * from './types';
@@ -9,106 +11,40 @@ export * from './calendar-item-wrapper';
9
11
  export * from './calendar-item';
10
12
  const DEFAULT_SLOTS = Array.from({ length: 96 }, (_, i) => i);
11
13
  const CurrentTimeLine = () => {
12
- const [now, setNow] = useState(new Date());
13
- useEffect(() => {
14
- const interval = setInterval(() => {
15
- setNow(new Date());
16
- }, 60000);
17
- return () => clearInterval(interval);
18
- }, []);
19
- const topOffset = useMemo(() => {
20
- const hours = now.getHours();
21
- const minutes = now.getMinutes();
22
- const totalMinutes = hours * 60 + minutes;
23
- // 15 минут = 20 пикселей (высота слота)
24
- return totalMinutes * (20 / 15);
25
- }, [now]);
14
+ const { topOffset } = useCurrentTime();
26
15
  return (_jsxs("div", { className: "absolute left-0 right-0 z-[101] pointer-events-none flex items-center", style: { top: `${topOffset}px` }, children: [_jsx("div", { className: "w-2 h-2 rounded-full bg-red-500 -ml-1" }), _jsx("div", { className: "flex-1 h-[2px] bg-red-500" })] }));
27
16
  };
28
17
  export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = true, autoScrollToCurrentTime = true, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
29
18
  const containerRef = useRef(null);
30
- const [isDragging, setIsDragging] = useState(false);
19
+ const [draggingTask, setDraggingTask] = useState(null);
31
20
  const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
32
21
  const tasksBySlot = useMemo(() => groupTasksBySlot(tasksWithPosition), [tasksWithPosition]);
33
- useEffect(() => {
34
- if (showCurrentTime && autoScrollToCurrentTime && containerRef.current) {
35
- const scroll = () => {
36
- if (!containerRef.current)
37
- return;
38
- const now = new Date();
39
- const hours = now.getHours();
40
- const minutes = now.getMinutes();
41
- const totalMinutes = hours * 60 + minutes;
42
- // 15 минут = 20 пикселей (высота слота)
43
- const topOffset = totalMinutes * (20 / 15);
44
- // Находим ближайший скроллируемый родитель или используем окно
45
- const scrollParent = (node) => {
46
- if (!node)
47
- return null;
48
- const style = window.getComputedStyle(node);
49
- if (/(auto|scroll)/.test(style.overflow + style.overflowY))
50
- return node;
51
- return scrollParent(node.parentElement);
52
- };
53
- const parent = scrollParent(containerRef.current);
54
- if (parent) {
55
- const containerRect = containerRef.current.getBoundingClientRect();
56
- const parentRect = parent.getBoundingClientRect();
57
- const relativeTop = containerRect.top - parentRect.top + parent.scrollTop;
58
- parent.scrollTo({
59
- top: relativeTop + topOffset - parent.clientHeight / 2,
60
- behavior: 'smooth',
61
- });
62
- }
63
- };
64
- const timeoutId = setTimeout(scroll, 100);
65
- return () => clearTimeout(timeoutId);
66
- }
67
- }, [showCurrentTime, autoScrollToCurrentTime]);
22
+ const { topOffset } = useCurrentTime();
23
+ useAutoScroll(containerRef, showCurrentTime && autoScrollToCurrentTime, topOffset);
68
24
  const handleDragStart = (e, task) => {
69
- console.log('CalendarLike: handleDragStart', task.id);
70
- setIsDragging(true);
25
+ setDraggingTask(task);
71
26
  e.dataTransfer.setData('taskId', task.id);
72
27
  e.dataTransfer.setData('taskTitle', task.title || '');
73
28
  e.dataTransfer.setData('taskEstimatedTime', String(task.estimatedTime));
74
29
  if (task.color) {
75
30
  e.dataTransfer.setData('taskColor', task.color);
76
31
  }
77
- // Сохраняем в глобальную переменную для доступа во время dragOver
78
- // eslint-disable-next-line react-hooks/immutability
79
- window._draggingTask = task;
80
32
  };
81
33
  const handleDragEnd = () => {
82
- setIsDragging(false);
83
- delete window._draggingTask;
34
+ setDraggingTask(null);
84
35
  };
85
36
  const handleDrop = (e, hour, minutes) => {
86
- console.log('CalendarLike: handleDrop triggered', { hour, minutes });
87
- setIsDragging(false);
88
- // Пытаемся получить taskId из dataTransfer
89
- let taskId = e.dataTransfer.getData('taskId');
90
- // Если в dataTransfer пусто (бывает в некоторых браузерах/ситуациях),
91
- // берем из глобальной переменной, которую мы засетили в handleDragStart
92
- if (!taskId && window._draggingTask) {
93
- taskId = window._draggingTask.id;
94
- console.log('CalendarLike: taskId recovered from _draggingTask:', taskId);
95
- }
96
- console.log('CalendarLike: taskId for drop:', taskId);
37
+ const taskId = e.dataTransfer.getData('taskId') || draggingTask?.id;
38
+ setDraggingTask(null);
97
39
  if (taskId && onTaskDrop) {
98
40
  onTaskDrop(taskId, hour, minutes);
99
41
  }
100
- else {
101
- console.warn('CalendarLike: taskId or onTaskDrop missing', { taskId, hasOnTaskDrop: !!onTaskDrop });
102
- }
103
- // Очищаем глобальную переменную после дропа
104
- delete window._draggingTask;
105
42
  };
106
- return (_jsxs("div", { ref: containerRef, className: jc('relative border-l border-secondary ml-12 select-none', isDragging ? 'is-dragging' : ''), children: [showCurrentTime && _jsx(CurrentTimeLine, {}), slots.map((slotIndex) => {
43
+ return (_jsxs("div", { ref: containerRef, className: jc('relative border-l border-secondary ml-12 select-none', draggingTask ? 'is-dragging' : ''), children: [showCurrentTime && _jsx(CurrentTimeLine, {}), slots.map((slotIndex) => {
107
44
  const tasksForSlot = tasksBySlot[slotIndex];
108
45
  const currentSlotTime = slotIndex * 15; // в минутах от начала дня
109
46
  // Считаем сколько задач пересекают этот слот (для кластеризации превью)
110
47
  // Исключаем текущую перетаскиваемую задачу из подсчета, чтобы превью не считало себя помехой
111
- const draggingTask = window._draggingTask;
112
48
  const overlappingTasksCount = tasks.filter((task) => {
113
49
  if (draggingTask && task.id === draggingTask.id)
114
50
  return false;
@@ -116,6 +52,6 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
116
52
  const taskEnd = taskStart + task.estimatedTime;
117
53
  return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
118
54
  }).length;
119
- return (_jsx(CalendarSlot, { slotIndex: slotIndex, onDrop: handleDrop, onCreateTask: onCreateTask, renderTask: renderTask, overlappingTasksCount: overlappingTasksCount, 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));
55
+ 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));
120
56
  })] }));
121
57
  };
@@ -73,9 +73,20 @@ export const Interactive = {
73
73
  return t;
74
74
  }));
75
75
  };
76
- const handleTaskResize = (taskId, newEstimatedTime) => {
77
- console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes`);
78
- setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, estimatedTime: newEstimatedTime } : t)));
76
+ const handleTaskResize = (taskId, newEstimatedTime, newStartHour, newStartMinutes) => {
77
+ console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes. New start: ${newStartHour}:${newStartMinutes}`);
78
+ setTasks((prev) => prev.map((t) => {
79
+ if (t.id === taskId) {
80
+ const newTask = { ...t, estimatedTime: newEstimatedTime };
81
+ if (newStartHour !== undefined && newStartMinutes !== undefined) {
82
+ const newDate = new Date(t.dueDate);
83
+ newDate.setHours(newStartHour, newStartMinutes, 0, 0);
84
+ newTask.dueDate = newDate;
85
+ }
86
+ return newTask;
87
+ }
88
+ return t;
89
+ }));
79
90
  };
80
91
  const handleCreateTask = (hour, minutes, estimatedTime) => {
81
92
  console.log(`Create task at ${hour}:${minutes} with duration ${estimatedTime}`);
@@ -92,7 +103,7 @@ export const Interactive = {
92
103
  console.log(`Task ${taskId} clicked`);
93
104
  alert(`Клик по задаче ${taskId}`);
94
105
  };
95
- return (_jsxs("div", { style: { height: '600px', overflowY: 'auto', background: 'var(--bg-primary)', padding: '20px' }, children: [_jsxs("p", { style: { marginBottom: '20px', color: 'var(--secondary)' }, children: ["\u0418\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044F: ", _jsx("br", {}), "1. \u041F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043C\u0435\u0436\u0434\u0443 \u0447\u0430\u0441\u0430\u043C\u0438. ", _jsx("br", {}), "2. \u0418\u0437\u043C\u0435\u043D\u044F\u0439\u0442\u0435 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438, \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u0437\u0430 \u043D\u0438\u0436\u043D\u0438\u0439 \u043A\u0440\u0430\u0439. ", _jsx("br", {}), "3. \u0421\u043E\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043B\u0438\u043A\u043D\u0443\u0432 \u0438 \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u043D\u0430 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u043E\u043C \u043C\u0435\u0441\u0442\u0435. ", _jsx("br", {}), "\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0432\u044B\u0432\u043E\u0434\u044F\u0442\u0441\u044F \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u044C."] }), _jsxs("div", { style: { marginBottom: '20px', color: 'var(--secondary)', display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "checkbox", checked: showCompleted, onChange: (e) => setShowCompleted(e.target.checked), id: "show-completed" }), _jsx("label", { htmlFor: "show-completed", style: { cursor: 'pointer' }, children: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435" })] }), _jsx(CalendarLike, { tasks: filteredTasks, onTaskDrop: handleTaskDrop, onTaskResize: handleTaskResize, onCreateTask: handleCreateTask, renderTask: (task) => {
106
+ return (_jsxs("div", { style: { height: '600px', overflowY: 'auto', background: 'var(--bg-primary)', padding: '20px' }, children: [_jsxs("p", { style: { marginBottom: '20px', color: 'var(--secondary)' }, children: ["\u0418\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044F: ", _jsx("br", {}), "1. \u041F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043C\u0435\u0436\u0434\u0443 \u0447\u0430\u0441\u0430\u043C\u0438. ", _jsx("br", {}), "2. \u0418\u0437\u043C\u0435\u043D\u044F\u0439\u0442\u0435 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438, \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u0437\u0430 \u043D\u0438\u0436\u043D\u0438\u0439 \u043A\u0440\u0430\u0439. ", _jsx("br", {}), "3. \u0418\u0437\u043C\u0435\u043D\u044F\u0439\u0442\u0435 \u0432\u0440\u0435\u043C\u044F \u043D\u0430\u0447\u0430\u043B\u0430 \u0438 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C, \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u0437\u0430 \u0432\u0435\u0440\u0445\u043D\u0438\u0439 \u043A\u0440\u0430\u0439. ", _jsx("br", {}), "4. \u0421\u043E\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438, \u043A\u043B\u0438\u043A\u043D\u0443\u0432 \u0438 \u043F\u043E\u0442\u044F\u043D\u0443\u0432 \u043D\u0430 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u043E\u043C \u043C\u0435\u0441\u0442\u0435. ", _jsx("br", {}), "\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0432\u044B\u0432\u043E\u0434\u044F\u0442\u0441\u044F \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u044C."] }), _jsxs("div", { style: { marginBottom: '20px', color: 'var(--secondary)', display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "checkbox", checked: showCompleted, onChange: (e) => setShowCompleted(e.target.checked), id: "show-completed" }), _jsx("label", { htmlFor: "show-completed", style: { cursor: 'pointer' }, children: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435" })] }), _jsx(CalendarLike, { tasks: filteredTasks, onTaskDrop: handleTaskDrop, onTaskResize: handleTaskResize, onCreateTask: handleCreateTask, renderTask: (task) => {
96
107
  const isCompleted = task.isCompleted;
97
108
  return (_jsx(CalendarItem, { title: _jsx(Cell, { subtitle: _jsx(Typo, { size: 'xs', weight: '500', children: "CODE-1" }), title: _jsx(Typo, { weight: "500", textDecoration: isCompleted ? 'line-through' : 'none', className: "truncate", children: task.title }), accLeft: [
98
109
  {
@@ -7,5 +7,6 @@ export type TCalendarSlotProps = {
7
7
  renderTask?: (task: TCalendarTask) => ReactNode;
8
8
  children?: ReactNode;
9
9
  overlappingTasksCount?: number;
10
+ draggingTask?: TCalendarTask;
10
11
  };
11
12
  export declare const CalendarSlot: FC<TCalendarSlotProps>;
@@ -3,7 +3,7 @@ import { animated, config, useSpring } from '@react-spring/web';
3
3
  import { useState } from 'react';
4
4
  import { jc } from '../../../utils';
5
5
  import { Typo } from '../text/typo';
6
- export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, children, overlappingTasksCount = 0, }) => {
6
+ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, children, overlappingTasksCount = 0, draggingTask, }) => {
7
7
  const hour = Math.floor(slotIndex / 4);
8
8
  const minutes = (slotIndex % 4) * 15;
9
9
  const isTopSlot = slotIndex % 4 === 0;
@@ -22,8 +22,6 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
22
22
  const handleDragOver = (e) => {
23
23
  e.preventDefault();
24
24
  e.dataTransfer.dropEffect = 'move';
25
- // Пытаемся получить данные из глобальной переменной
26
- const draggingTask = window._draggingTask;
27
25
  if (!draggingTask)
28
26
  return;
29
27
  if (!dragOverInfo) {
@@ -37,7 +35,6 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
37
35
  setDragOverInfo(null);
38
36
  };
39
37
  const handleInternalDrop = (e) => {
40
- console.log('CalendarSlot: handleInternalDrop triggered', { hour, minutes });
41
38
  setDragOverInfo(null);
42
39
  onDrop?.(e, hour, minutes); // Приклеиваем к началу слота (15-минутка)
43
40
  };
@@ -21,7 +21,7 @@ export type TCalendarLikeProps = {
21
21
  autoScrollToCurrentTime?: boolean;
22
22
  renderTask: (task: TCalendarTask) => ReactNode;
23
23
  onTaskDrop?: (taskId: string, hour: number, minutes: number) => void;
24
- onTaskResize?: (taskId: string, newEstimatedTime: number) => void;
24
+ onTaskResize?: (taskId: string, newEstimatedTime: number, newStartHour?: number, newStartMinutes?: number) => void;
25
25
  onTaskClick?: (taskId: string) => void;
26
26
  onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
27
27
  };
@@ -0,0 +1,2 @@
1
+ import { RefObject } from 'react';
2
+ export declare const useAutoScroll: (containerRef: RefObject<HTMLDivElement | null>, enabled: boolean, topOffset: number) => void;
@@ -0,0 +1,49 @@
1
+ import { useEffect } from 'react';
2
+ export const useAutoScroll = (containerRef, enabled, topOffset) => {
3
+ useEffect(() => {
4
+ if (enabled && containerRef.current) {
5
+ const scroll = (retryCount = 0) => {
6
+ if (!containerRef.current)
7
+ return;
8
+ const now = new Date();
9
+ const hours = now.getHours();
10
+ const minutes = now.getMinutes();
11
+ const totalMinutes = hours * 60 + minutes;
12
+ // Находим ближайший скроллируемый родитель или используем окно
13
+ const scrollParent = (node) => {
14
+ if (!node)
15
+ return null;
16
+ const style = window.getComputedStyle(node);
17
+ const overflow = style.overflow + style.overflowY + style.overflowX;
18
+ if (/(auto|scroll)/.test(overflow))
19
+ return node;
20
+ return scrollParent(node.parentElement);
21
+ };
22
+ const parent = scrollParent(containerRef.current);
23
+ if (parent) {
24
+ const getRelativeOffsetTop = (target, ancestor) => {
25
+ let offset = 0;
26
+ let current = target;
27
+ while (current && current !== ancestor) {
28
+ offset += current.offsetTop;
29
+ current = current.offsetParent;
30
+ }
31
+ return offset;
32
+ };
33
+ const relativeTop = getRelativeOffsetTop(containerRef.current, parent);
34
+ // Если relativeTop равен 0 и это не начало дня, возможно DOM еще не готов
35
+ if (relativeTop === 0 && totalMinutes > 60 && retryCount < 5) {
36
+ setTimeout(() => scroll(retryCount + 1), 100);
37
+ return;
38
+ }
39
+ parent.scrollTo({
40
+ top: relativeTop + topOffset - parent.clientHeight / 2,
41
+ behavior: 'smooth',
42
+ });
43
+ }
44
+ };
45
+ const timeoutId = setTimeout(() => scroll(0), 100);
46
+ return () => clearTimeout(timeoutId);
47
+ }
48
+ }, [enabled, topOffset, containerRef]);
49
+ };
@@ -0,0 +1,4 @@
1
+ export declare const useCurrentTime: (updateInterval?: number) => {
2
+ now: Date;
3
+ topOffset: number;
4
+ };
@@ -0,0 +1,18 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ export const useCurrentTime = (updateInterval = 60000) => {
3
+ const [now, setNow] = useState(new Date());
4
+ useEffect(() => {
5
+ const interval = setInterval(() => {
6
+ setNow(new Date());
7
+ }, updateInterval);
8
+ return () => clearInterval(interval);
9
+ }, [updateInterval]);
10
+ const topOffset = useMemo(() => {
11
+ const hours = now.getHours();
12
+ const minutes = now.getMinutes();
13
+ const totalMinutes = hours * 60 + minutes;
14
+ // 15 минут = 20 пикселей (высота слота)
15
+ return totalMinutes * (20 / 15);
16
+ }, [now]);
17
+ return { now, topOffset };
18
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkkarsss/ui",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "UI Kit for kkkarsss projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",