@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.
- package/dist/ui/information/calendar-like/calendar-item-wrapper.d.ts +1 -1
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +56 -16
- package/dist/ui/information/calendar-like/calendar-like.js +13 -99
- package/dist/ui/information/calendar-like/calendar-like.stories.js +15 -4
- package/dist/ui/information/calendar-like/calendar-slot.d.ts +1 -0
- package/dist/ui/information/calendar-like/calendar-slot.js +1 -4
- package/dist/ui/information/calendar-like/types.d.ts +1 -1
- package/dist/ui/information/calendar-like/use-auto-scroll.d.ts +2 -0
- package/dist/ui/information/calendar-like/use-auto-scroll.js +49 -0
- package/dist/ui/information/calendar-like/use-current-time.d.ts +4 -0
- package/dist/ui/information/calendar-like/use-current-time.js +18 -0
- package/dist/ui/layout/main-page-layout/main-page-layout.js +1 -1
- package/package.json +1 -1
|
@@ -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 [
|
|
6
|
-
const
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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]',
|
|
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,
|
|
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
|
|
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 [
|
|
19
|
+
const [draggingTask, setDraggingTask] = useState(null);
|
|
31
20
|
const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
|
|
32
21
|
const tasksBySlot = useMemo(() => groupTasksBySlot(tasksWithPosition), [tasksWithPosition]);
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
delete window._draggingTask;
|
|
34
|
+
setDraggingTask(null);
|
|
106
35
|
};
|
|
107
36
|
const handleDrop = (e, hour, minutes) => {
|
|
108
|
-
|
|
109
|
-
|
|
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',
|
|
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) =>
|
|
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
|
{
|
|
@@ -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,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,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
|
};
|