@kkkarsss/ui 1.4.10 → 1.4.12

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,128 +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 = (retryCount = 0) => {
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
- const overflow = style.overflow + style.overflowY + style.overflowX;
50
- if (/(auto|scroll)/.test(overflow))
51
- return node;
52
- return scrollParent(node.parentElement);
53
- };
54
- const parent = scrollParent(containerRef.current);
55
- if (parent) {
56
- const getRelativeOffsetTop = (target, ancestor) => {
57
- let offset = 0;
58
- let current = target;
59
- while (current && current !== ancestor) {
60
- offset += current.offsetTop;
61
- current = current.offsetParent;
62
- }
63
- return offset;
64
- };
65
- const relativeTop = getRelativeOffsetTop(containerRef.current, parent);
66
- // Если relativeTop равен 0 и это не начало дня, возможно DOM еще не готов
67
- if (relativeTop === 0 && totalMinutes > 60 && retryCount < 5) {
68
- setTimeout(() => scroll(retryCount + 1), 100);
69
- return;
70
- }
71
- console.log('CalendarLike scroll:', {
72
- totalMinutes,
73
- topOffset,
74
- parentScrollTop: parent.scrollTop,
75
- parentClientHeight: parent.clientHeight,
76
- relativeTop,
77
- retryCount,
78
- scrollTarget: relativeTop + topOffset - parent.clientHeight / 2,
79
- });
80
- parent.scrollTo({
81
- top: relativeTop + topOffset - parent.clientHeight / 2,
82
- behavior: 'smooth',
83
- });
84
- }
85
- };
86
- const timeoutId = setTimeout(() => scroll(0), 100);
87
- return () => clearTimeout(timeoutId);
88
- }
89
- }, [showCurrentTime, autoScrollToCurrentTime]);
22
+ const { topOffset } = useCurrentTime();
23
+ useAutoScroll(containerRef, showCurrentTime && autoScrollToCurrentTime, topOffset);
90
24
  const handleDragStart = (e, task) => {
91
- console.log('CalendarLike: handleDragStart', task.id);
92
- setIsDragging(true);
25
+ setDraggingTask(task);
93
26
  e.dataTransfer.setData('taskId', task.id);
94
27
  e.dataTransfer.setData('taskTitle', task.title || '');
95
28
  e.dataTransfer.setData('taskEstimatedTime', String(task.estimatedTime));
96
29
  if (task.color) {
97
30
  e.dataTransfer.setData('taskColor', task.color);
98
31
  }
99
- // Сохраняем в глобальную переменную для доступа во время dragOver
100
- // eslint-disable-next-line react-hooks/immutability
101
- window._draggingTask = task;
102
32
  };
103
33
  const handleDragEnd = () => {
104
- setIsDragging(false);
105
- delete window._draggingTask;
34
+ setDraggingTask(null);
106
35
  };
107
36
  const handleDrop = (e, hour, minutes) => {
108
- console.log('CalendarLike: handleDrop triggered', { hour, minutes });
109
- setIsDragging(false);
110
- // Пытаемся получить taskId из dataTransfer
111
- let taskId = e.dataTransfer.getData('taskId');
112
- // Если в dataTransfer пусто (бывает в некоторых браузерах/ситуациях),
113
- // берем из глобальной переменной, которую мы засетили в handleDragStart
114
- if (!taskId && window._draggingTask) {
115
- taskId = window._draggingTask.id;
116
- console.log('CalendarLike: taskId recovered from _draggingTask:', taskId);
117
- }
118
- console.log('CalendarLike: taskId for drop:', taskId);
37
+ const taskId = e.dataTransfer.getData('taskId') || draggingTask?.id;
38
+ setDraggingTask(null);
119
39
  if (taskId && onTaskDrop) {
120
40
  onTaskDrop(taskId, hour, minutes);
121
41
  }
122
- else {
123
- console.warn('CalendarLike: taskId or onTaskDrop missing', { taskId, hasOnTaskDrop: !!onTaskDrop });
124
- }
125
- // Очищаем глобальную переменную после дропа
126
- delete window._draggingTask;
127
42
  };
128
- 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) => {
129
44
  const tasksForSlot = tasksBySlot[slotIndex];
130
45
  const currentSlotTime = slotIndex * 15; // в минутах от начала дня
131
46
  // Считаем сколько задач пересекают этот слот (для кластеризации превью)
132
47
  // Исключаем текущую перетаскиваемую задачу из подсчета, чтобы превью не считало себя помехой
133
- const draggingTask = window._draggingTask;
134
48
  const overlappingTasksCount = tasks.filter((task) => {
135
49
  if (draggingTask && task.id === draggingTask.id)
136
50
  return false;
@@ -138,6 +52,6 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
138
52
  const taskEnd = taskStart + task.estimatedTime;
139
53
  return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
140
54
  }).length;
141
- 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));
142
56
  })] }));
143
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
+ };
@@ -8,5 +8,5 @@ import { IconAction } from '../icon-action/icon-action';
8
8
  export const MainPageLayout = ({ children, leftPanel, rightPanel, leftIcon, rightIcon, }) => {
9
9
  const { widget } = useWidget();
10
10
  const { expandedPanel, toggleLeftPanel, toggleRightPanel } = useLayout();
11
- return (_jsx("div", { className: "@container w-full h-[100dvh]", children: _jsxs("main", { className: "grid h-full w-full grid-cols-1 @s:grid-cols-[300px_1fr_300px] @s:py-xl @s:px-l @s:gap-l py-m px-s gap-s relative overflow-hidden", children: [_jsx("aside", { className: "hidden @s:block h-full w-full", children: leftPanel }), _jsx("div", { className: "@s:hidden absolute left-4 top-4 z-50", children: _jsx(IconAction, { icon: expandedPanel === 'left' ? _jsx(X, { size: 20 }) : leftIcon || _jsx(Menu, { size: 20 }), onClick: toggleLeftPanel }) }), _jsxs(Flex, { justify: 'center', gap: '16px', type: 'fill', className: jc(expandedPanel ? 'hidden @s:flex' : 'flex', 'h-full mt-[50px] @s:m-0'), children: [children, widget && _jsx(Fragment, { children: widget.ui }, widget.id)] }), _jsx("aside", { className: "hidden @s:block h-full w-full", children: rightPanel }), _jsx("div", { className: "@s:hidden absolute right-4 top-4 z-50", children: _jsx(IconAction, { icon: expandedPanel === 'right' ? _jsx(X, { size: 20 }) : rightIcon || _jsx(Menu, { size: 20 }), onClick: toggleRightPanel }) }), expandedPanel && (_jsx("div", { className: "@s:hidden absolute inset-0 z-40 bg-background pt-16 px-4 pb-4 overflow-auto", children: expandedPanel === 'left' ? leftPanel : rightPanel }))] }) }));
11
+ return (_jsx("div", { className: "@container w-full h-[100dvh]", children: _jsxs("main", { className: "grid h-full w-full grid-cols-1 @s:grid-cols-[300px_1fr_300px] @s:py-xl @s:px-l @s:gap-l py-m px-s gap-s relative overflow-hidden", children: [_jsx("aside", { className: "hidden @s:block h-full w-full", children: leftPanel }), _jsx("div", { className: "@s:hidden absolute left-4 top-4 z-50", children: _jsx(IconAction, { icon: expandedPanel === 'left' ? _jsx(X, { size: 20 }) : leftIcon || _jsx(Menu, { size: 20 }), onClick: toggleLeftPanel }) }), _jsxs(Flex, { justify: 'center', gap: '16px', type: 'fill', className: jc(expandedPanel ? 'hidden @s:flex' : 'flex', 'h-full mt-[50px] @s:m-0'), children: [_jsx("div", { className: 'hidden @m:flex', children: children }), widget && _jsx(Fragment, { children: widget.ui }, widget.id)] }), _jsx("aside", { className: "hidden @s:block h-full w-full", children: rightPanel }), _jsx("div", { className: "@s:hidden absolute right-4 top-4 z-50", children: _jsx(IconAction, { icon: expandedPanel === 'right' ? _jsx(X, { size: 20 }) : rightIcon || _jsx(Menu, { size: 20 }), onClick: toggleRightPanel }) }), expandedPanel && (_jsx("div", { className: "@s:hidden absolute inset-0 z-40 bg-background pt-16 px-4 pb-4 overflow-auto", children: expandedPanel === 'left' ? leftPanel : rightPanel }))] }) }));
12
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkkarsss/ui",
3
- "version": "1.4.10",
3
+ "version": "1.4.12",
4
4
  "description": "UI Kit for kkkarsss projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",