@kkkarsss/ui 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/information/calendar-like/calendar-item-wrapper.d.ts +1 -0
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +18 -7
- package/dist/ui/information/calendar-like/calendar-like.js +32 -11
- package/dist/ui/information/calendar-like/calendar-like.stories.js +4 -3
- package/dist/ui/information/calendar-like/{calendar-hour-slot.d.ts → calendar-slot.d.ts} +3 -3
- package/dist/ui/information/calendar-like/calendar-slot.js +70 -0
- package/dist/ui/information/calendar-like/index.d.ts +0 -1
- package/dist/ui/information/calendar-like/index.js +0 -1
- package/dist/ui/information/calendar-like/types.d.ts +1 -0
- package/dist/ui/information/calendar-like/utils.d.ts +1 -1
- package/dist/ui/information/calendar-like/utils.js +12 -6
- package/package.json +9 -9
- package/dist/ui/information/calendar-like/calendar-hour-slot.js +0 -64
|
@@ -9,6 +9,7 @@ export type TCalendarItemWrapperProps = {
|
|
|
9
9
|
height: string;
|
|
10
10
|
};
|
|
11
11
|
onDragStart?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
12
|
+
onDragEnd?: (e: DragEvent<HTMLDivElement>, id: string) => void;
|
|
12
13
|
onResize?: (id: string, newEstimatedTime: number) => void;
|
|
13
14
|
renderTask: (task: TCalendarTask) => ReactNode;
|
|
14
15
|
};
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { jc } from '../../../utils';
|
|
4
|
-
export const CalendarItemWrapper = ({ task, position, onDragStart, onResize, renderTask, }) => {
|
|
4
|
+
export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, onResize, renderTask, }) => {
|
|
5
5
|
const [previewEstimatedTime, setPreviewEstimatedTime] = useState(null);
|
|
6
6
|
const handleMouseDown = (e) => {
|
|
7
7
|
if (onResize) {
|
|
8
8
|
e.preventDefault();
|
|
9
9
|
e.stopPropagation();
|
|
10
10
|
const startY = e.clientY;
|
|
11
|
-
const startHeight = (task.estimatedTime /
|
|
11
|
+
const startHeight = (task.estimatedTime / 15) * 20;
|
|
12
12
|
const onMouseMove = (moveEvent) => {
|
|
13
13
|
const deltaY = moveEvent.clientY - startY;
|
|
14
14
|
const newHeight = Math.max(20, startHeight + deltaY);
|
|
15
|
-
|
|
15
|
+
// Округляем до 15 минут (20px)
|
|
16
|
+
const newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
|
|
16
17
|
setPreviewEstimatedTime(newEstimatedTime);
|
|
17
18
|
};
|
|
18
19
|
const onMouseUp = (upEvent) => {
|
|
19
20
|
const deltaY = upEvent.clientY - startY;
|
|
20
21
|
const newHeight = Math.max(20, startHeight + deltaY);
|
|
21
|
-
const newEstimatedTime = Math.max(15, Math.round(
|
|
22
|
+
const newEstimatedTime = Math.max(15, Math.round(newHeight / 20) * 15);
|
|
22
23
|
if (newEstimatedTime !== task.estimatedTime) {
|
|
23
24
|
onResize(task.id, newEstimatedTime);
|
|
24
25
|
}
|
|
@@ -35,22 +36,32 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onResize, ren
|
|
|
35
36
|
document.addEventListener('mouseup', onMouseUp);
|
|
36
37
|
}
|
|
37
38
|
};
|
|
38
|
-
const displayHeight = previewEstimatedTime ? `${(previewEstimatedTime /
|
|
39
|
+
const displayHeight = previewEstimatedTime ? `${(previewEstimatedTime / 15) * 20}px` : position.height;
|
|
39
40
|
return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
|
|
40
41
|
onDragStart?.(e, task.id);
|
|
41
42
|
const el = e.currentTarget;
|
|
43
|
+
// Создаем пустой элемент для скрытия стандартного ghost image браузера
|
|
44
|
+
const img = new Image();
|
|
45
|
+
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
46
|
+
e.dataTransfer.setDragImage(img, 0, 0);
|
|
42
47
|
if (el) {
|
|
43
|
-
|
|
48
|
+
// Используем setTimeout, чтобы прозрачность и pointer-events применились после того, как браузер создаст ghost image
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
el.style.opacity = '0';
|
|
51
|
+
el.style.pointerEvents = 'none';
|
|
52
|
+
}, 0);
|
|
44
53
|
}
|
|
45
54
|
}, onDragEnd: (e) => {
|
|
55
|
+
onDragEnd?.(e, task.id);
|
|
46
56
|
const el = e.currentTarget;
|
|
47
57
|
if (el) {
|
|
48
58
|
el.style.opacity = '';
|
|
59
|
+
el.style.pointerEvents = '';
|
|
49
60
|
}
|
|
50
61
|
}, style: {
|
|
51
62
|
height: displayHeight,
|
|
52
63
|
width: position.width,
|
|
53
64
|
left: position.left,
|
|
54
65
|
top: position.top,
|
|
55
|
-
}, className: jc('calendar-item absolute p-[2px] transition-
|
|
66
|
+
}, 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]' : ''), 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" }) }))] }) }));
|
|
56
67
|
};
|
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import { CalendarHourSlot } from './calendar-hour-slot';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
4
3
|
import { CalendarItemWrapper } from './calendar-item-wrapper';
|
|
5
|
-
import {
|
|
4
|
+
import { CalendarSlot } from './calendar-slot';
|
|
5
|
+
import { calculateTaskPositions, groupTasksBySlot } from './utils';
|
|
6
|
+
import { jc } from '../../../utils';
|
|
6
7
|
export * from './types';
|
|
7
8
|
export * from './calendar-item-wrapper';
|
|
8
9
|
export * from './calendar-item';
|
|
9
|
-
const
|
|
10
|
-
export const CalendarLike = ({ tasks,
|
|
10
|
+
const DEFAULT_SLOTS = Array.from({ length: 96 }, (_, i) => i);
|
|
11
|
+
export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
|
|
12
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
11
13
|
const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
|
|
12
|
-
const
|
|
13
|
-
const handleDragStart = (e,
|
|
14
|
-
|
|
14
|
+
const tasksBySlot = useMemo(() => groupTasksBySlot(tasksWithPosition), [tasksWithPosition]);
|
|
15
|
+
const handleDragStart = (e, task) => {
|
|
16
|
+
setIsDragging(true);
|
|
17
|
+
e.dataTransfer.setData('taskId', task.id);
|
|
18
|
+
e.dataTransfer.setData('taskTitle', task.title || '');
|
|
19
|
+
e.dataTransfer.setData('taskEstimatedTime', String(task.estimatedTime));
|
|
20
|
+
if (task.color) {
|
|
21
|
+
e.dataTransfer.setData('taskColor', task.color);
|
|
22
|
+
}
|
|
23
|
+
// Сохраняем в глобальную переменную для доступа во время dragOver
|
|
24
|
+
// eslint-disable-next-line react-hooks/immutability
|
|
25
|
+
window._draggingTask = {
|
|
26
|
+
id: task.id,
|
|
27
|
+
title: task.title,
|
|
28
|
+
estimatedTime: task.estimatedTime,
|
|
29
|
+
color: task.color,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const handleDragEnd = () => {
|
|
33
|
+
setIsDragging(false);
|
|
34
|
+
delete window._draggingTask;
|
|
15
35
|
};
|
|
16
36
|
const handleDrop = (e, hour, minutes) => {
|
|
37
|
+
setIsDragging(false);
|
|
17
38
|
const taskId = e.dataTransfer.getData('taskId');
|
|
18
39
|
if (taskId && onTaskDrop) {
|
|
19
40
|
onTaskDrop(taskId, hour, minutes);
|
|
20
41
|
}
|
|
21
42
|
};
|
|
22
|
-
return (_jsx("div", { className:
|
|
23
|
-
const
|
|
24
|
-
return (_jsx(
|
|
43
|
+
return (_jsx("div", { className: jc('relative border-l border-secondary/20 ml-12 select-none', isDragging ? 'is-dragging' : ''), children: slots.map((slotIndex) => {
|
|
44
|
+
const tasksForSlot = tasksBySlot[slotIndex];
|
|
45
|
+
return (_jsx(CalendarSlot, { slotIndex: slotIndex, onDrop: handleDrop, onCreateTask: onCreateTask, 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));
|
|
25
46
|
}) }));
|
|
26
47
|
};
|
|
@@ -58,12 +58,13 @@ export const Interactive = {
|
|
|
58
58
|
const [tasks, setTasks] = useState(INITIAL_TASKS);
|
|
59
59
|
const [showCompleted, setShowCompleted] = useState(true);
|
|
60
60
|
const filteredTasks = tasks.filter((t) => (showCompleted ? true : !t.isCompleted));
|
|
61
|
-
const handleTaskDrop = (taskId, hour) => {
|
|
62
|
-
console.log(`Task ${taskId} dropped at
|
|
61
|
+
const handleTaskDrop = (taskId, hour, minutes) => {
|
|
62
|
+
console.log(`Task ${taskId} dropped at ${hour}:${minutes}`);
|
|
63
63
|
setTasks((prev) => prev.map((t) => {
|
|
64
64
|
if (t.id === taskId) {
|
|
65
65
|
const newDate = new Date(t.dueDate);
|
|
66
|
-
|
|
66
|
+
// Storybook handles the date update
|
|
67
|
+
newDate.setHours(hour, minutes, 0, 0);
|
|
67
68
|
return { ...t, dueDate: newDate };
|
|
68
69
|
}
|
|
69
70
|
return t;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type FC, type DragEvent, ReactNode } from 'react';
|
|
2
|
-
export type
|
|
3
|
-
|
|
2
|
+
export type TCalendarSlotProps = {
|
|
3
|
+
slotIndex: number;
|
|
4
4
|
onDrop?: (e: DragEvent<HTMLDivElement>, hour: number, minutes: number) => void;
|
|
5
5
|
onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
|
|
6
6
|
children?: ReactNode;
|
|
7
7
|
};
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const CalendarSlot: FC<TCalendarSlotProps>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Typo } from '../text/typo';
|
|
4
|
+
export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, children }) => {
|
|
5
|
+
const hour = Math.floor(slotIndex / 4);
|
|
6
|
+
const minutes = (slotIndex % 4) * 15;
|
|
7
|
+
const isTopSlot = slotIndex % 4 === 0;
|
|
8
|
+
const [selection, setSelection] = useState(null);
|
|
9
|
+
const [dragOverInfo, setDragOverInfo] = useState(null);
|
|
10
|
+
const handleDragOver = (e) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
// Пытаемся получить данные из глобальной переменной
|
|
13
|
+
const draggingTask = window._draggingTask;
|
|
14
|
+
const title = draggingTask?.title || e.dataTransfer.getData('taskTitle');
|
|
15
|
+
const estimatedTime = draggingTask?.estimatedTime || Number(e.dataTransfer.getData('taskEstimatedTime')) || 15;
|
|
16
|
+
const color = draggingTask?.color || e.dataTransfer.getData('taskColor');
|
|
17
|
+
setDragOverInfo({
|
|
18
|
+
title,
|
|
19
|
+
estimatedTime,
|
|
20
|
+
color,
|
|
21
|
+
topOffset: 0, // Приклеиваем к началу слота (15-минутка)
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const handleDragLeave = () => {
|
|
25
|
+
setDragOverInfo(null);
|
|
26
|
+
};
|
|
27
|
+
const handleInternalDrop = (e) => {
|
|
28
|
+
setDragOverInfo(null);
|
|
29
|
+
onDrop?.(e, hour, minutes); // Приклеиваем к началу слота (15-минутка)
|
|
30
|
+
};
|
|
31
|
+
const handleMouseDown = (e) => {
|
|
32
|
+
if (e.button !== 0 || !onCreateTask)
|
|
33
|
+
return;
|
|
34
|
+
if (e.target.closest('.calendar-item'))
|
|
35
|
+
return;
|
|
36
|
+
setSelection({
|
|
37
|
+
startMinutes: minutes,
|
|
38
|
+
currentEstimatedTime: 15,
|
|
39
|
+
});
|
|
40
|
+
const startY = e.clientY;
|
|
41
|
+
const onMouseMove = (moveEvent) => {
|
|
42
|
+
const deltaY = moveEvent.clientY - startY;
|
|
43
|
+
// Округляем до 15 минут (20px)
|
|
44
|
+
const newEstimatedTime = Math.max(15, Math.round(deltaY / 20) * 15 + 15);
|
|
45
|
+
setSelection((prev) => (prev ? { ...prev, currentEstimatedTime: newEstimatedTime } : null));
|
|
46
|
+
};
|
|
47
|
+
const onMouseUp = () => {
|
|
48
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
49
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
50
|
+
setSelection((finalSelection) => {
|
|
51
|
+
if (finalSelection) {
|
|
52
|
+
onCreateTask(hour, minutes, finalSelection.currentEstimatedTime);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
58
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("div", { onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleInternalDrop, onMouseDown: handleMouseDown, className: `relative h-5 group hover:z-10 has-[.calendar-item:hover]:z-30 has-[.calendar-item:active]:z-30 ${isTopSlot ? 'border-t border-t-secondary/20' : ''}`, children: [isTopSlot && (_jsx("div", { className: "absolute -left-12 top-0 -translate-y-1/2 w-10 text-right", children: _jsxs(Typo, { size: "xs", color: "secondary", children: [String(hour).padStart(2, '0'), ":00"] }) })), _jsxs("div", { className: "pl-4 relative h-full", children: [children, dragOverInfo !== null && (_jsx("div", { className: "absolute left-4 right-1 rounded-sm z-[40] pointer-events-none flex flex-col overflow-hidden border border-accent transition-colors", style: {
|
|
61
|
+
top: `${dragOverInfo.topOffset}px`,
|
|
62
|
+
height: `${(dragOverInfo.estimatedTime / 15) * 20}px`,
|
|
63
|
+
minHeight: '20px',
|
|
64
|
+
backgroundColor: dragOverInfo.color ? `${dragOverInfo.color}80` : 'var(--accent)',
|
|
65
|
+
borderColor: dragOverInfo.color ? dragOverInfo.color : 'var(--accent)',
|
|
66
|
+
}, children: _jsx("div", { className: "p-1", children: _jsx(Typo, { size: "s", weight: "500", className: "truncate leading-none", children: dragOverInfo.title || 'Переместить сюда' }) }) })), selection && (_jsx("div", { className: "absolute left-4 right-1 bg-accent/30 border border-accent rounded-sm z-[40] pointer-events-none", style: {
|
|
67
|
+
top: '0px',
|
|
68
|
+
height: `${(selection.currentEstimatedTime / 15) * 20}px`,
|
|
69
|
+
}, children: _jsxs("div", { className: "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"] })] }) }))] })] }));
|
|
70
|
+
};
|
|
@@ -16,6 +16,7 @@ export type TCalendarTaskWithPosition = TCalendarTask & {
|
|
|
16
16
|
export type TCalendarLikeProps = {
|
|
17
17
|
tasks: TCalendarTask[];
|
|
18
18
|
hours?: number[];
|
|
19
|
+
slots?: number[];
|
|
19
20
|
renderTask: (task: TCalendarTask) => ReactNode;
|
|
20
21
|
onTaskDrop?: (taskId: string, hour: number, minutes: number) => void;
|
|
21
22
|
onTaskResize?: (taskId: string, newEstimatedTime: number) => void;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { TCalendarTask, TCalendarTaskWithPosition } from './types';
|
|
2
2
|
export declare const calculateTaskPositions: (tasks: TCalendarTask[]) => TCalendarTaskWithPosition[];
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const groupTasksBySlot: (tasksWithPosition: TCalendarTaskWithPosition[]) => Record<number, TCalendarTaskWithPosition[]>;
|
|
@@ -54,8 +54,12 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
54
54
|
column.forEach((task) => {
|
|
55
55
|
const width = clusterColumnsCount > 1 ? `${100 / clusterColumnsCount}%` : '100%';
|
|
56
56
|
const left = clusterColumnsCount > 1 ? `${(colIndex * 100) / clusterColumnsCount}%` : '0%';
|
|
57
|
-
|
|
58
|
-
const
|
|
57
|
+
// Расчитываем смещение внутри 15-минутного слота
|
|
58
|
+
const minutes = task.dueDate.getMinutes();
|
|
59
|
+
const minutesInSlot = minutes % 15;
|
|
60
|
+
const topOffset = (minutesInSlot / 15) * 20;
|
|
61
|
+
const top = `${Math.round(topOffset)}px`; // Округляем для надежности, но по идее minutesInSlot должен быть 0 если все по сетке
|
|
62
|
+
const height = `${Math.round((Math.max(15, task.estimatedTime || 15) / 15) * 20)}px`;
|
|
59
63
|
result.push({
|
|
60
64
|
...task,
|
|
61
65
|
position: { width, left, top, height },
|
|
@@ -65,13 +69,15 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
65
69
|
});
|
|
66
70
|
return result;
|
|
67
71
|
};
|
|
68
|
-
export const
|
|
72
|
+
export const groupTasksBySlot = (tasksWithPosition) => {
|
|
69
73
|
const map = {};
|
|
70
74
|
tasksWithPosition.forEach((task) => {
|
|
71
75
|
const hour = task.dueDate.getHours();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
map[
|
|
76
|
+
const minutes = task.dueDate.getMinutes();
|
|
77
|
+
const slotIndex = hour * 4 + Math.floor(minutes / 15);
|
|
78
|
+
if (!map[slotIndex])
|
|
79
|
+
map[slotIndex] = [];
|
|
80
|
+
map[slotIndex].push(task);
|
|
75
81
|
});
|
|
76
82
|
return map;
|
|
77
83
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kkkarsss/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "UI Kit for kkkarsss projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@chromatic-com/storybook": "^5.0.0",
|
|
28
28
|
"@eslint/js": "^9.39.2",
|
|
29
|
-
"@storybook/addon-a11y": "^10.2.
|
|
30
|
-
"@storybook/addon-docs": "^10.2.
|
|
31
|
-
"@storybook/addon-onboarding": "^10.2.
|
|
32
|
-
"@storybook/addon-vitest": "^10.2.
|
|
33
|
-
"@storybook/react": "^10.2.
|
|
34
|
-
"@storybook/react-vite": "^10.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
35
|
"@types/node": "^25.0.10",
|
|
36
36
|
"@types/react": "^19.2.9",
|
|
37
37
|
"@types/react-dom": "^19.2.3",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"eslint-plugin-import": "^2.32.0",
|
|
45
45
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
46
46
|
"eslint-plugin-react-refresh": "^0.4.26",
|
|
47
|
-
"eslint-plugin-storybook": "^10.2.
|
|
47
|
+
"eslint-plugin-storybook": "^10.2.7",
|
|
48
48
|
"globals": "^16.5.0",
|
|
49
49
|
"lucide-react": "^0.563.0",
|
|
50
50
|
"playwright": "^1.58.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prettier": "^3.8.1",
|
|
53
53
|
"react": "^19.2.3",
|
|
54
54
|
"react-dom": "^19.2.3",
|
|
55
|
-
"storybook": "^10.2.
|
|
55
|
+
"storybook": "^10.2.7",
|
|
56
56
|
"tailwindcss": "^3.4.19",
|
|
57
57
|
"typescript": "5.9.3",
|
|
58
58
|
"typescript-eslint": "^8.53.1",
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Typo } from '../text/typo';
|
|
4
|
-
export const CalendarHourSlot = ({ hour, onDrop, onCreateTask, children }) => {
|
|
5
|
-
const [selection, setSelection] = useState(null);
|
|
6
|
-
const [dragOverMinutes, setDragOverMinutes] = useState(null);
|
|
7
|
-
const handleDragOver = (e) => {
|
|
8
|
-
e.preventDefault();
|
|
9
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
10
|
-
const offsetY = e.clientY - rect.top;
|
|
11
|
-
const minutes = Math.floor(offsetY / 20) * 15;
|
|
12
|
-
if (minutes !== dragOverMinutes) {
|
|
13
|
-
setDragOverMinutes(minutes);
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
const handleDragLeave = () => {
|
|
17
|
-
setDragOverMinutes(null);
|
|
18
|
-
};
|
|
19
|
-
const handleInternalDrop = (e) => {
|
|
20
|
-
setDragOverMinutes(null);
|
|
21
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
22
|
-
const offsetY = e.clientY - rect.top;
|
|
23
|
-
const minutes = Math.floor(offsetY / 20) * 15;
|
|
24
|
-
onDrop?.(e, hour, minutes);
|
|
25
|
-
};
|
|
26
|
-
const handleMouseDown = (e) => {
|
|
27
|
-
if (e.button !== 0 || !onCreateTask)
|
|
28
|
-
return;
|
|
29
|
-
if (e.target.closest('.calendar-item'))
|
|
30
|
-
return;
|
|
31
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
32
|
-
const offsetY = e.clientY - rect.top;
|
|
33
|
-
const startMinutes = Math.floor(offsetY / 20) * 15;
|
|
34
|
-
setSelection({
|
|
35
|
-
startMinutes,
|
|
36
|
-
currentEstimatedTime: 15,
|
|
37
|
-
});
|
|
38
|
-
const startY = e.clientY;
|
|
39
|
-
const onMouseMove = (moveEvent) => {
|
|
40
|
-
const deltaY = moveEvent.clientY - startY;
|
|
41
|
-
const newEstimatedTime = Math.max(15, Math.round(((deltaY / 80) * 60) / 15) * 15);
|
|
42
|
-
setSelection((prev) => (prev ? { ...prev, currentEstimatedTime: newEstimatedTime } : null));
|
|
43
|
-
};
|
|
44
|
-
const onMouseUp = () => {
|
|
45
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
46
|
-
document.removeEventListener('mouseup', onMouseUp);
|
|
47
|
-
setSelection((finalSelection) => {
|
|
48
|
-
if (finalSelection) {
|
|
49
|
-
onCreateTask(hour, finalSelection.startMinutes, finalSelection.currentEstimatedTime);
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
});
|
|
53
|
-
};
|
|
54
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
55
|
-
document.addEventListener('mouseup', onMouseUp);
|
|
56
|
-
};
|
|
57
|
-
return (_jsxs("div", { onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleInternalDrop, onMouseDown: handleMouseDown, className: "relative h-20 border-b border-secondary-foreground group hover:z-10 has-[.calendar-item:hover]:z-30 has-[.calendar-item:active]:z-30", children: [_jsx("div", { className: "absolute -left-12 top-0 -translate-y-1/2 w-10 text-right", children: _jsxs(Typo, { size: "xs", color: "secondary", children: [String(hour).padStart(2, '0'), ":00"] }) }), _jsxs("div", { className: "pl-4 py-1 relative h-full", children: [children, dragOverMinutes !== null && (_jsx("div", { className: "absolute left-4 right-1 bg-accent/10 border border-dashed border-accent/50 rounded-sm z-[40] pointer-events-none", style: {
|
|
58
|
-
top: `${(dragOverMinutes / 60) * 80}px`,
|
|
59
|
-
height: '20px',
|
|
60
|
-
} })), selection && (_jsx("div", { className: "absolute left-4 right-1 bg-accent/30 border border-accent rounded-sm z-[40] pointer-events-none", style: {
|
|
61
|
-
top: `${(selection.startMinutes / 60) * 80}px`,
|
|
62
|
-
height: `${(selection.currentEstimatedTime / 60) * 80}px`,
|
|
63
|
-
}, children: _jsxs("div", { className: "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"] })] }) }))] })] }));
|
|
64
|
-
};
|