@kkkarsss/ui 1.2.1 → 1.3.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.
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { animated, useSpring, config } from '@react-spring/web';
3
+ import { useState } from 'react';
2
4
  import { jc } from '../../../utils';
3
5
  import { Typo } from '../../information';
4
6
  const variantMap = {
@@ -7,5 +9,10 @@ const variantMap = {
7
9
  selling: 'bg-accent text-accent-foreground',
8
10
  };
9
11
  export const Button = ({ children, onClick, variant = 'active', disabled = false }) => {
10
- return (_jsx("button", { className: jc('w-auto px-4 py-2 text-[14px] font-medium border border-transparent cursor-pointer outline-none transition-opacity duration-200', 'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none hover:not-disabled:opacity-90', variantMap[variant]), onClick: onClick, disabled: disabled, children: _jsx(Typo, { size: 'm', children: children }) }));
12
+ const [pressed, setPressed] = useState(false);
13
+ const springs = useSpring({
14
+ transform: pressed ? 'scale(0.96)' : 'scale(1)',
15
+ config: config.stiff,
16
+ });
17
+ return (_jsx(animated.button, { style: springs, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), className: jc('w-auto px-4 py-2 text-[14px] font-medium border border-transparent cursor-pointer outline-none', 'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none hover:not-disabled:opacity-90', variantMap[variant]), onClick: onClick, disabled: disabled, children: _jsx(Typo, { size: 'm', children: children }) }));
11
18
  };
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Activity, createElement } from 'react';
2
+ import { animated, config, useSpring } from '@react-spring/web';
3
+ import { Activity, createElement, useState } from 'react';
3
4
  import { jc } from '../../../utils';
4
5
  const variantMap = {
5
6
  default: 'bg-secondary text-secondary-foreground',
@@ -7,5 +8,10 @@ const variantMap = {
7
8
  secondary: 'bg-transparent text-secondary-foreground border border-secondary-foreground',
8
9
  };
9
10
  export const Chip = ({ children, onClick, accLeft, variant = 'default' }) => {
10
- return (_jsxs("button", { onClick: onClick, className: jc('flex items-center text-nowrap gap-[5px] w-min px-2 py-1 rounded-2xl outline-none border-none cursor-pointer text-[14px]', variantMap[variant]), children: [_jsx(Activity, { mode: accLeft ? 'visible' : 'hidden', children: accLeft ? createElement(accLeft, { size: 14, color: 'white', strokeWidth: 3 }) : null }), children] }));
11
+ const [pressed, setPressed] = useState(false);
12
+ const springs = useSpring({
13
+ transform: pressed ? 'scale(0.95)' : 'scale(1)',
14
+ config: config.stiff,
15
+ });
16
+ return (_jsxs(animated.button, { style: springs, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onClick: onClick, className: jc('flex items-center text-nowrap gap-[5px] w-min px-2 py-1 rounded-2xl outline-none border-none cursor-pointer text-[14px]', variantMap[variant]), children: [_jsx(Activity, { mode: accLeft ? 'visible' : 'hidden', children: accLeft ? createElement(accLeft, { size: 14, color: 'white', strokeWidth: 3 }) : null }), children] }));
11
17
  };
@@ -9,5 +9,6 @@ export type TBlockProps = PropsWithChildren<{
9
9
  maxHeight?: number | string;
10
10
  minWidth?: number | string;
11
11
  minHeight?: number | string;
12
+ animate?: boolean;
12
13
  }>;
13
14
  export declare const Block: FC<TBlockProps>;
@@ -1,10 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { animated, config, useSpring } from '@react-spring/web';
2
3
  import { jc } from '../../../utils';
3
4
  import { Flex, Offset } from '../../layout';
4
5
  const typeMap = {
5
6
  fill: 'h-full',
6
7
  hug: 'h-auto',
7
8
  };
8
- export const Block = ({ children, title, topAcc, filters, type = 'fill', maxWidth, maxHeight, minWidth, minHeight, }) => {
9
- return (_jsxs("div", { className: jc('w-full flex flex-col', 'rounded-2xl', 'bg-background-accent', 'shadow-[0_0_8px_var(--shadow)]', 'scrollbar-none', 'overflow-hidden', typeMap[type]), style: { maxWidth, maxHeight, minWidth, minHeight }, children: [_jsx("div", { className: jc('sticky', 'top-0', 'z-20', 'bg-background-accent', 'w-full', 'shrink-0'), children: _jsxs(Flex, { direction: 'column', children: [_jsxs(Flex, { justify: 'space-between', align: 'center', children: [title && _jsx(Offset, { type: 'both', children: title }), _jsx("div", { className: 'mx-m', children: _jsx(Flex, { direction: 'column', children: _jsx(Flex, { align: 'center', gap: '4px', children: topAcc }) }) })] }), filters && _jsx("div", { className: 'mb-l', children: filters })] }) }), _jsx("div", { className: "flex-1 overflow-auto scrollbar-none", children: _jsx(Flex, { direction: 'column', type: 'fill', children: children }) })] }));
9
+ export const Block = ({ children, title, topAcc, filters, type = 'fill', maxWidth, maxHeight, minWidth, minHeight, animate = true, }) => {
10
+ const springs = useSpring({
11
+ from: { opacity: 0, transform: 'translateY(10px)' },
12
+ to: { opacity: 1, transform: 'translateY(0px)' },
13
+ config: config.gentle,
14
+ immediate: !animate,
15
+ });
16
+ return (_jsxs(animated.div, { className: jc('w-full flex flex-col', 'rounded-2xl', 'bg-background-accent', 'shadow-[0_0_8px_var(--shadow)]', 'scrollbar-none', 'overflow-hidden', typeMap[type]), style: { ...springs, maxWidth, maxHeight, minWidth, minHeight }, children: [_jsx("div", { className: jc('sticky', 'top-0', 'z-20', 'bg-background-accent', 'w-full', 'shrink-0'), children: _jsxs(Flex, { direction: 'column', children: [_jsxs(Flex, { justify: 'space-between', align: 'center', children: [title && _jsx(Offset, { type: 'both', children: title }), _jsx("div", { className: 'mx-m', children: _jsx(Flex, { direction: 'column', children: _jsx(Flex, { align: 'center', gap: '4px', children: topAcc }) }) })] }), filters && _jsx("div", { className: 'mb-l', children: filters })] }) }), _jsx("div", { className: "flex-1 overflow-auto scrollbar-none", children: _jsx(Flex, { direction: 'column', type: 'fill', children: children }) })] }));
10
17
  };
@@ -12,5 +12,6 @@ export type TCalendarItemWrapperProps = {
12
12
  onDragEnd?: (e: DragEvent<HTMLDivElement>, id: string) => void;
13
13
  onResize?: (id: string, newEstimatedTime: number) => void;
14
14
  renderTask: (task: TCalendarTask) => ReactNode;
15
+ overlappingTasksCount?: number;
15
16
  };
16
17
  export declare const CalendarItemWrapper: FC<TCalendarItemWrapperProps>;
@@ -38,6 +38,7 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
38
38
  };
39
39
  const displayHeight = previewEstimatedTime ? `${(previewEstimatedTime / 15) * 20}px` : position.height;
40
40
  return (_jsx("div", { draggable: !!onDragStart, onDragStart: (e) => {
41
+ console.log('CalendarItemWrapper: onDragStart', task.id);
41
42
  onDragStart?.(e, task.id);
42
43
  const el = e.currentTarget;
43
44
  // Создаем пустой элемент для скрытия стандартного ghost image браузера
@@ -47,11 +48,12 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
47
48
  if (el) {
48
49
  // Используем setTimeout, чтобы прозрачность и pointer-events применились после того, как браузер создаст ghost image
49
50
  setTimeout(() => {
50
- el.style.opacity = '0';
51
+ el.style.opacity = '0.4';
51
52
  el.style.pointerEvents = 'none';
52
53
  }, 0);
53
54
  }
54
55
  }, onDragEnd: (e) => {
56
+ console.log('CalendarItemWrapper: onDragEnd');
55
57
  onDragEnd?.(e, task.id);
56
58
  const el = e.currentTarget;
57
59
  if (el) {
@@ -63,5 +65,5 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
63
65
  width: position.width,
64
66
  left: position.left,
65
67
  top: position.top,
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" }) }))] }) }));
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" }) }))] }) }));
67
69
  };
@@ -1,10 +1,7 @@
1
1
  import { type FC, type ReactNode } from 'react';
2
2
  export type TCalendarItemProps = {
3
3
  title: ReactNode;
4
- isCompleted?: boolean;
5
- isInWork?: boolean;
6
4
  onClick?: () => void;
7
- icon?: ReactNode;
8
5
  color?: string;
9
6
  className?: string;
10
7
  };
@@ -1,14 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ExternalLink } from 'lucide-react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { animated, config, useSpring } from '@react-spring/web';
3
+ import { useState } from 'react';
3
4
  import { jc } from '../../../utils';
4
5
  import { Flex } from '../../layout';
5
- import { Typo } from '../text/typo';
6
- export const CalendarItem = ({ title, isCompleted, isInWork, onClick, icon, color, className, }) => {
7
- return (_jsx("div", { className: jc('w-full h-full rounded-[8px] border flex flex-col transition-colors relative', !color && (isCompleted ? 'border-secondary bg-accent' : 'border-accent bg-accent'), className), style: {
8
- backgroundColor: color ? color : undefined,
9
- borderColor: color ? color : undefined,
10
- }, children: _jsxs(Flex, { gap: "8px", align: "center", justify: "space-between", type: "fill", children: [_jsxs(Flex, { gap: "8px", align: "center", type: "fill", className: "min-w-0 flex-1", children: [typeof title === 'string' ? (_jsx(Typo, { size: "s", weight: "500", textDecoration: isCompleted ? 'line-through' : 'none', className: "truncate", children: title })) : (title), isInWork && icon] }), onClick && (_jsx("div", { onClick: (e) => {
11
- e.stopPropagation();
12
- onClick();
13
- }, className: "cursor-pointer hover:text-primary transition-colors pr-l", children: _jsx(ExternalLink, { size: 14 }) }))] }) }));
6
+ export const CalendarItem = ({ title, color, className, onClick }) => {
7
+ const [pressed, setPressed] = useState(false);
8
+ const springs = useSpring({
9
+ transform: pressed ? 'scale(0.98)' : 'scale(1)',
10
+ config: config.stiff,
11
+ });
12
+ return (_jsx(animated.div, { style: {
13
+ ...springs,
14
+ backgroundColor: color ? `${color}ac` : undefined,
15
+ border: 0,
16
+ borderLeft: `3px solid ${color}`,
17
+ }, onMouseDown: () => setPressed(true), onMouseUp: () => setPressed(false), onMouseLeave: () => setPressed(false), onClick: onClick, className: jc('w-full h-full rounded-[8px] border flex flex-col transition-colors relative', onClick && 'cursor-pointer', className), children: _jsx(Flex, { gap: "8px", align: "center", justify: "space-between", type: "fill", children: _jsx(Flex, { gap: "8px", align: "center", type: "fill", className: "min-w-0 flex-1", children: title }) }) }));
14
18
  };
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState, useEffect, useRef } from 'react';
3
3
  import { CalendarItemWrapper } from './calendar-item-wrapper';
4
4
  import { CalendarSlot } from './calendar-slot';
5
5
  import { calculateTaskPositions, groupTasksBySlot } from './utils';
@@ -8,11 +8,57 @@ export * from './types';
8
8
  export * from './calendar-item-wrapper';
9
9
  export * from './calendar-item';
10
10
  const DEFAULT_SLOTS = Array.from({ length: 96 }, (_, i) => i);
11
- export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
11
+ 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]);
26
+ 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
+ };
28
+ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = true, autoScrollToCurrentTime = true, renderTask, onTaskDrop, onTaskResize, onCreateTask, }) => {
29
+ const containerRef = useRef(null);
12
30
  const [isDragging, setIsDragging] = useState(false);
13
31
  const tasksWithPosition = useMemo(() => calculateTaskPositions(tasks), [tasks]);
14
32
  const tasksBySlot = useMemo(() => groupTasksBySlot(tasksWithPosition), [tasksWithPosition]);
33
+ useEffect(() => {
34
+ if (showCurrentTime && autoScrollToCurrentTime && containerRef.current) {
35
+ const now = new Date();
36
+ const hours = now.getHours();
37
+ const minutes = now.getMinutes();
38
+ const totalMinutes = hours * 60 + minutes;
39
+ // 15 минут = 20 пикселей (высота слота)
40
+ const topOffset = totalMinutes * (20 / 15);
41
+ // Находим ближайший скроллируемый родитель или используем окно
42
+ const scrollParent = (node) => {
43
+ if (!node)
44
+ return null;
45
+ const style = window.getComputedStyle(node);
46
+ if (/(auto|scroll)/.test(style.overflow + style.overflowY))
47
+ return node;
48
+ return scrollParent(node.parentElement);
49
+ };
50
+ const parent = scrollParent(containerRef.current);
51
+ if (parent) {
52
+ const containerTop = containerRef.current.getBoundingClientRect().top + parent.scrollTop;
53
+ parent.scrollTo({
54
+ top: containerTop + topOffset - parent.clientHeight / 2,
55
+ behavior: 'smooth',
56
+ });
57
+ }
58
+ }
59
+ }, [showCurrentTime, autoScrollToCurrentTime]);
15
60
  const handleDragStart = (e, task) => {
61
+ console.log('CalendarLike: handleDragStart', task.id);
16
62
  setIsDragging(true);
17
63
  e.dataTransfer.setData('taskId', task.id);
18
64
  e.dataTransfer.setData('taskTitle', task.title || '');
@@ -22,26 +68,46 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, renderTask, onTaskD
22
68
  }
23
69
  // Сохраняем в глобальную переменную для доступа во время dragOver
24
70
  // 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
- };
71
+ window._draggingTask = task;
31
72
  };
32
73
  const handleDragEnd = () => {
33
74
  setIsDragging(false);
34
75
  delete window._draggingTask;
35
76
  };
36
77
  const handleDrop = (e, hour, minutes) => {
78
+ console.log('CalendarLike: handleDrop triggered', { hour, minutes });
37
79
  setIsDragging(false);
38
- const taskId = e.dataTransfer.getData('taskId');
80
+ // Пытаемся получить taskId из dataTransfer
81
+ let taskId = e.dataTransfer.getData('taskId');
82
+ // Если в dataTransfer пусто (бывает в некоторых браузерах/ситуациях),
83
+ // берем из глобальной переменной, которую мы засетили в handleDragStart
84
+ if (!taskId && window._draggingTask) {
85
+ taskId = window._draggingTask.id;
86
+ console.log('CalendarLike: taskId recovered from _draggingTask:', taskId);
87
+ }
88
+ console.log('CalendarLike: taskId for drop:', taskId);
39
89
  if (taskId && onTaskDrop) {
40
90
  onTaskDrop(taskId, hour, minutes);
41
91
  }
92
+ else {
93
+ console.warn('CalendarLike: taskId or onTaskDrop missing', { taskId, hasOnTaskDrop: !!onTaskDrop });
94
+ }
95
+ // Очищаем глобальную переменную после дропа
96
+ delete window._draggingTask;
42
97
  };
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));
46
- }) }));
98
+ 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) => {
99
+ const tasksForSlot = tasksBySlot[slotIndex];
100
+ const currentSlotTime = slotIndex * 15; // в минутах от начала дня
101
+ // Считаем сколько задач пересекают этот слот (для кластеризации превью)
102
+ // Исключаем текущую перетаскиваемую задачу из подсчета, чтобы превью не считало себя помехой
103
+ const draggingTask = window._draggingTask;
104
+ const overlappingTasksCount = tasks.filter((task) => {
105
+ if (draggingTask && task.id === draggingTask.id)
106
+ return false;
107
+ const taskStart = task.dueDate.getHours() * 60 + task.dueDate.getMinutes();
108
+ const taskEnd = taskStart + task.estimatedTime;
109
+ return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
110
+ }).length;
111
+ 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));
112
+ })] }));
47
113
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Circle, CircleCheckBig, Clock } from 'lucide-react';
2
+ import { Circle, CircleCheckBig } from 'lucide-react';
3
3
  import { useState } from 'react';
4
4
  import { CalendarLike, CalendarItem } from './calendar-like';
5
5
  import { Cell } from '../cell/cell';
@@ -53,6 +53,7 @@ const INITIAL_TASKS = [
53
53
  dueDate: getTodayAtHour(17),
54
54
  estimatedTime: 30,
55
55
  isCompleted: true,
56
+ color: '#8b5cf6',
56
57
  },
57
58
  ];
58
59
  export const Interactive = {
@@ -96,14 +97,14 @@ export const Interactive = {
96
97
  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: [
97
98
  {
98
99
  icon: isCompleted ? CircleCheckBig : Circle,
99
- color: 'var(--accent)',
100
+ color: 'white',
100
101
  strokeWidth: 3,
101
102
  onClick: (e) => {
102
103
  e?.stopPropagation();
103
104
  setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, isCompleted: !t.isCompleted } : t)));
104
105
  },
105
106
  },
106
- ] }), color: task.color, isCompleted: isCompleted, isInWork: task.isInWork, icon: task.isInWork ? _jsx(Clock, { size: 12, className: "text-white animate-pulse" }) : undefined, onClick: () => handleTaskClick(task.id) }));
107
+ ] }), color: task.color, onClick: () => handleTaskClick(task.id) }));
107
108
  } })] }));
108
109
  },
109
110
  };
@@ -1,8 +1,11 @@
1
1
  import { type FC, type DragEvent, ReactNode } from 'react';
2
+ import { TCalendarTask } from './types';
2
3
  export type TCalendarSlotProps = {
3
4
  slotIndex: number;
4
5
  onDrop?: (e: DragEvent<HTMLDivElement>, hour: number, minutes: number) => void;
5
6
  onCreateTask?: (hour: number, minutes: number, estimatedTime: number) => void;
7
+ renderTask?: (task: TCalendarTask) => ReactNode;
6
8
  children?: ReactNode;
9
+ overlappingTasksCount?: number;
7
10
  };
8
11
  export declare const CalendarSlot: FC<TCalendarSlotProps>;
@@ -1,30 +1,43 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { animated, config, useSpring } from '@react-spring/web';
2
3
  import { useState } from 'react';
4
+ import { jc } from '../../../utils';
3
5
  import { Typo } from '../text/typo';
4
- export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, children }) => {
6
+ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, children, overlappingTasksCount = 0, }) => {
5
7
  const hour = Math.floor(slotIndex / 4);
6
8
  const minutes = (slotIndex % 4) * 15;
7
9
  const isTopSlot = slotIndex % 4 === 0;
8
10
  const [selection, setSelection] = useState(null);
9
11
  const [dragOverInfo, setDragOverInfo] = useState(null);
12
+ const springs = useSpring({
13
+ to: {
14
+ opacity: dragOverInfo ? 1 : 0.6,
15
+ height: dragOverInfo ? `${(dragOverInfo.task.estimatedTime / 15) * 20}px` : '40px',
16
+ },
17
+ config: { ...config.stiff, precision: 0.001 },
18
+ });
19
+ const handleDragEnter = (e) => {
20
+ e.preventDefault();
21
+ };
10
22
  const handleDragOver = (e) => {
11
23
  e.preventDefault();
24
+ e.dataTransfer.dropEffect = 'move';
12
25
  // Пытаемся получить данные из глобальной переменной
13
26
  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
- });
27
+ if (!draggingTask)
28
+ return;
29
+ if (!dragOverInfo) {
30
+ setDragOverInfo({
31
+ task: draggingTask,
32
+ topOffset: 0, // Приклеиваем к началу слота (15-минутка)
33
+ });
34
+ }
23
35
  };
24
36
  const handleDragLeave = () => {
25
37
  setDragOverInfo(null);
26
38
  };
27
39
  const handleInternalDrop = (e) => {
40
+ console.log('CalendarSlot: handleInternalDrop triggered', { hour, minutes });
28
41
  setDragOverInfo(null);
29
42
  onDrop?.(e, hour, minutes); // Приклеиваем к началу слота (15-минутка)
30
43
  };
@@ -57,14 +70,22 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, children }) => {
57
70
  document.addEventListener('mousemove', onMouseMove);
58
71
  document.addEventListener('mouseup', onMouseUp);
59
72
  };
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: {
73
+ return (_jsxs("div", { onDragEnter: handleDragEnter, 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' : ''}`, 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(animated.div, { className: jc('absolute z-[100] pointer-events-none flex flex-col overflow-hidden'), style: {
74
+ ...springs,
61
75
  top: `${dragOverInfo.topOffset}px`,
62
- height: `${(dragOverInfo.estimatedTime / 15) * 20}px`,
63
76
  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: {
77
+ width: overlappingTasksCount > 0 ? `${100 / (overlappingTasksCount + 1)}%` : '100%',
78
+ left: overlappingTasksCount > 0 ? `${(overlappingTasksCount * 100) / (overlappingTasksCount + 1)}%` : '0',
79
+ }, children: renderTask ? (renderTask(dragOverInfo.task)) : (_jsx("div", { className: "p-1", children: _jsx(Typo, { size: "s", weight: "500", className: "truncate leading-none", children: dragOverInfo.task.title || 'Переместить сюда' }) })) })), selection && (_jsx("div", { className: "absolute left-4 right-1 z-[40] pointer-events-none", style: {
67
80
  top: '0px',
68
81
  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"] })] }) }))] })] }));
82
+ width: overlappingTasksCount > 0 ? `${100 / (overlappingTasksCount + 1)}%` : '100%',
83
+ left: overlappingTasksCount > 0 ? `${(overlappingTasksCount * 100) / (overlappingTasksCount + 1)}%` : '0',
84
+ }, children: renderTask ? (renderTask({
85
+ id: 'new-task-preview',
86
+ title: 'Новая задача',
87
+ dueDate: new Date(new Date().setHours(hour, minutes, 0, 0)),
88
+ estimatedTime: selection.currentEstimatedTime,
89
+ color: 'white',
90
+ })) : (_jsxs("div", { className: "border rounded-sm h-full p-1", children: [_jsx(Typo, { size: "xs", weight: "500", children: "\u041D\u043E\u0432\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430" }), _jsxs(Typo, { size: "xs", color: "secondary", children: [selection.currentEstimatedTime, " \u043C\u0438\u043D"] })] })) }))] })] }));
70
91
  };
@@ -17,6 +17,8 @@ export type TCalendarLikeProps = {
17
17
  tasks: TCalendarTask[];
18
18
  hours?: number[];
19
19
  slots?: number[];
20
+ showCurrentTime?: boolean;
21
+ autoScrollToCurrentTime?: boolean;
20
22
  renderTask: (task: TCalendarTask) => ReactNode;
21
23
  onTaskDrop?: (taskId: string, hour: number, minutes: number) => void;
22
24
  onTaskResize?: (taskId: string, newEstimatedTime: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkkarsss/ui",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "UI Kit for kkkarsss projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,6 +60,7 @@
60
60
  "vitest": "^4.0.18"
61
61
  },
62
62
  "dependencies": {
63
+ "@react-spring/web": "^10.0.3",
63
64
  "date-fns": "^4.1.0",
64
65
  "react-datepicker": "^9.1.0",
65
66
  "react-day-picker": "^9.13.0"