@kkkarsss/ui 1.5.4 → 1.5.6
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/controls/date-input/date-input.js +7 -4
- package/dist/ui/information/calendar-like/calendar-item-wrapper.js +40 -9
- package/dist/ui/information/calendar-like/calendar-like.js +35 -3
- package/dist/ui/information/calendar-like/calendar-like.stories.js +57 -25
- package/dist/ui/information/calendar-like/calendar-slot.js +42 -3
- package/dist/ui/information/calendar-like/types.d.ts +3 -2
- package/dist/ui/information/calendar-like/use-auto-scroll.js +4 -3
- package/dist/ui/information/calendar-like/use-current-time.d.ts +2 -1
- package/dist/ui/information/calendar-like/use-current-time.js +5 -4
- package/dist/ui/information/calendar-like/utils.js +49 -24
- package/dist/ui/layout/main-page-layout/main-page-layout.js +1 -1
- package/package.json +3 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { Calendar } from 'lucide-react';
|
|
3
4
|
import { useRef, useState } from 'react';
|
|
4
5
|
import { Dropdown } from '../../layout';
|
|
@@ -9,10 +10,12 @@ export const DateInput = ({ value, onChange, label, placeholder, size = 'm', dis
|
|
|
9
10
|
const containerRef = useRef(null);
|
|
10
11
|
const handleDateSelect = (date) => {
|
|
11
12
|
if (date) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const plainDate = Temporal.PlainDate.from({
|
|
14
|
+
year: date.getFullYear(),
|
|
15
|
+
month: date.getMonth() + 1,
|
|
16
|
+
day: date.getDate(),
|
|
17
|
+
});
|
|
18
|
+
onChange(plainDate.toString());
|
|
16
19
|
setIsOpen(false);
|
|
17
20
|
}
|
|
18
21
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { useState } from 'react';
|
|
3
4
|
import { jc } from '../../../utils';
|
|
4
5
|
export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, onResize, renderTask, }) => {
|
|
@@ -8,9 +9,36 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
8
9
|
e.preventDefault();
|
|
9
10
|
e.stopPropagation();
|
|
10
11
|
const startY = e.clientY;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const toISO = (d) => {
|
|
13
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
14
|
+
return d.toString().split('T')[0];
|
|
15
|
+
if (d instanceof Date)
|
|
16
|
+
return d.toISOString().split('T')[0];
|
|
17
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
18
|
+
const parsed = new Date(d);
|
|
19
|
+
if (!isNaN(parsed.getTime()))
|
|
20
|
+
return parsed.toISOString().split('T')[0];
|
|
21
|
+
}
|
|
22
|
+
return String(d);
|
|
23
|
+
};
|
|
24
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
25
|
+
? task.dueDateStart
|
|
26
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
27
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate
|
|
28
|
+
? task.dueDateEnd
|
|
29
|
+
: Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
30
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
31
|
+
? task.dueTimeStart
|
|
32
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
33
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
34
|
+
? task.dueTimeEnd
|
|
35
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
36
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
37
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
38
|
+
const estimatedTime = Number(dtEnd.since(dtStart).total('minute'));
|
|
39
|
+
const startHeight = (estimatedTime / 15) * 20;
|
|
40
|
+
const startHour = tStart.hour;
|
|
41
|
+
const startMinutes = tStart.minute;
|
|
14
42
|
const onMouseMove = (moveEvent) => {
|
|
15
43
|
const deltaY = moveEvent.clientY - startY;
|
|
16
44
|
let newHeight;
|
|
@@ -24,9 +52,9 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
24
52
|
else {
|
|
25
53
|
// Resizing top
|
|
26
54
|
const deltaMinutes = Math.round(deltaY / 20) * 15;
|
|
27
|
-
newEstimatedTime = Math.max(15,
|
|
55
|
+
newEstimatedTime = Math.max(15, estimatedTime - deltaMinutes);
|
|
28
56
|
// If we hit min height, don't move the top further down
|
|
29
|
-
const actualDeltaMinutes =
|
|
57
|
+
const actualDeltaMinutes = estimatedTime - newEstimatedTime;
|
|
30
58
|
const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
|
|
31
59
|
newStartHour = Math.floor(totalStartMinutes / 60);
|
|
32
60
|
newStartMinutes = totalStartMinutes % 60;
|
|
@@ -44,13 +72,13 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
44
72
|
}
|
|
45
73
|
else {
|
|
46
74
|
const deltaMinutes = Math.round(deltaY / 20) * 15;
|
|
47
|
-
finalEstimatedTime = Math.max(15,
|
|
48
|
-
const actualDeltaMinutes =
|
|
75
|
+
finalEstimatedTime = Math.max(15, estimatedTime - deltaMinutes);
|
|
76
|
+
const actualDeltaMinutes = estimatedTime - finalEstimatedTime;
|
|
49
77
|
const totalStartMinutes = startHour * 60 + startMinutes + actualDeltaMinutes;
|
|
50
78
|
finalStartHour = Math.floor(totalStartMinutes / 60);
|
|
51
79
|
finalStartMinutes = totalStartMinutes % 60;
|
|
52
80
|
}
|
|
53
|
-
if (finalEstimatedTime !==
|
|
81
|
+
if (finalEstimatedTime !== estimatedTime ||
|
|
54
82
|
finalStartHour !== startHour ||
|
|
55
83
|
finalStartMinutes !== startMinutes) {
|
|
56
84
|
onResize(task.id, finalEstimatedTime, finalStartHour, finalStartMinutes);
|
|
@@ -71,7 +99,10 @@ export const CalendarItemWrapper = ({ task, position, onDragStart, onDragEnd, on
|
|
|
71
99
|
const displayHeight = preview ? `${(preview.estimatedTime / 15) * 20}px` : position.height;
|
|
72
100
|
let displayTop = position.top;
|
|
73
101
|
if (preview && (preview.startHour !== undefined || preview.startMinutes !== undefined)) {
|
|
74
|
-
const
|
|
102
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
103
|
+
? task.dueTimeStart
|
|
104
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
105
|
+
const originalStartTotalMinutes = tStart.hour * 60 + tStart.minute;
|
|
75
106
|
const newStartTotalMinutes = preview.startHour * 60 + preview.startMinutes;
|
|
76
107
|
const diffMinutes = newStartTotalMinutes - originalStartTotalMinutes;
|
|
77
108
|
const diffPixels = (diffMinutes / 15) * 20;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { useMemo, useState, useRef } from 'react';
|
|
3
4
|
import { CalendarItemWrapper } from './calendar-item-wrapper';
|
|
4
5
|
import { CalendarSlot } from './calendar-slot';
|
|
@@ -25,7 +26,32 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
|
|
|
25
26
|
setDraggingTask(task);
|
|
26
27
|
e.dataTransfer.setData('taskId', task.id);
|
|
27
28
|
e.dataTransfer.setData('taskTitle', task.title || '');
|
|
28
|
-
|
|
29
|
+
const toISO = (d) => {
|
|
30
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
31
|
+
return d.toString().split('T')[0];
|
|
32
|
+
if (d instanceof Date)
|
|
33
|
+
return d.toISOString().split('T')[0];
|
|
34
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
35
|
+
const parsed = new Date(d);
|
|
36
|
+
if (!isNaN(parsed.getTime()))
|
|
37
|
+
return parsed.toISOString().split('T')[0];
|
|
38
|
+
}
|
|
39
|
+
return String(d);
|
|
40
|
+
};
|
|
41
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
42
|
+
? task.dueDateStart
|
|
43
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
44
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate ? task.dueDateEnd : Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
45
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
46
|
+
? task.dueTimeStart
|
|
47
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
48
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
49
|
+
? task.dueTimeEnd
|
|
50
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
51
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
52
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
53
|
+
const estimatedTime = Number(dtEnd.since(dtStart).total('minute'));
|
|
54
|
+
e.dataTransfer.setData('taskEstimatedTime', String(estimatedTime));
|
|
29
55
|
if (task.color) {
|
|
30
56
|
e.dataTransfer.setData('taskColor', task.color);
|
|
31
57
|
}
|
|
@@ -48,8 +74,14 @@ export const CalendarLike = ({ tasks, slots = DEFAULT_SLOTS, showCurrentTime = t
|
|
|
48
74
|
const overlappingTasksCount = tasks.filter((task) => {
|
|
49
75
|
if (draggingTask && task.id === draggingTask.id)
|
|
50
76
|
return false;
|
|
51
|
-
const
|
|
52
|
-
|
|
77
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
78
|
+
? task.dueTimeStart
|
|
79
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
80
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
81
|
+
? task.dueTimeEnd
|
|
82
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
83
|
+
const taskStart = tStart.hour * 60 + tStart.minute;
|
|
84
|
+
const taskEnd = tEnd.hour * 60 + tEnd.minute;
|
|
53
85
|
return currentSlotTime >= taskStart && currentSlotTime < taskEnd;
|
|
54
86
|
}).length;
|
|
55
87
|
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));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { Circle, CircleCheckBig } from 'lucide-react';
|
|
3
4
|
import { useState } from 'react';
|
|
4
5
|
import { CalendarLike, CalendarItem } from './calendar-like';
|
|
@@ -10,48 +11,59 @@ const meta = {
|
|
|
10
11
|
tags: ['autodocs'],
|
|
11
12
|
};
|
|
12
13
|
export default meta;
|
|
13
|
-
const getTodayAtHour = (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const getTodayAtHour = (_hour, _minutes = 0) => {
|
|
15
|
+
return Temporal.Now.plainDateISO();
|
|
16
|
+
};
|
|
17
|
+
const getTodayTimeAtHour = (hour, minutes = 0) => {
|
|
18
|
+
return Temporal.PlainTime.from({ hour, minute: minutes });
|
|
17
19
|
};
|
|
18
20
|
const INITIAL_TASKS = [
|
|
19
21
|
{
|
|
20
22
|
id: '1',
|
|
21
23
|
title: 'Утренняя почта',
|
|
22
24
|
description: 'Проверить входящие и ответить на важные письма',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
dueDateStart: getTodayAtHour(9),
|
|
26
|
+
dueDateEnd: getTodayAtHour(9),
|
|
27
|
+
dueTimeStart: getTodayTimeAtHour(9),
|
|
28
|
+
dueTimeEnd: getTodayTimeAtHour(9, 45),
|
|
25
29
|
color: '#3b82f6',
|
|
26
30
|
},
|
|
27
31
|
{
|
|
28
32
|
id: '2',
|
|
29
33
|
title: 'Стендап',
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
dueDateStart: getTodayAtHour(10),
|
|
35
|
+
dueDateEnd: getTodayAtHour(10),
|
|
36
|
+
dueTimeStart: getTodayTimeAtHour(10),
|
|
37
|
+
dueTimeEnd: getTodayTimeAtHour(10, 15),
|
|
32
38
|
color: '#10b981',
|
|
33
39
|
isInWork: true,
|
|
34
40
|
},
|
|
35
41
|
{
|
|
36
42
|
id: '3',
|
|
37
43
|
title: 'Обед',
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
dueDateStart: getTodayAtHour(13),
|
|
45
|
+
dueDateEnd: getTodayAtHour(13),
|
|
46
|
+
dueTimeStart: getTodayTimeAtHour(13),
|
|
47
|
+
dueTimeEnd: getTodayTimeAtHour(14),
|
|
40
48
|
color: '#f59e0b',
|
|
41
49
|
},
|
|
42
50
|
{
|
|
43
51
|
id: '4',
|
|
44
52
|
title: 'Разработка фичи',
|
|
45
53
|
description: 'Реализация логики календаря',
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
dueDateStart: getTodayAtHour(14),
|
|
55
|
+
dueDateEnd: getTodayAtHour(14),
|
|
56
|
+
dueTimeStart: getTodayTimeAtHour(14),
|
|
57
|
+
dueTimeEnd: getTodayTimeAtHour(16),
|
|
48
58
|
color: '#8b5cf6',
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
id: '5',
|
|
52
62
|
title: 'Завершенная задача',
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
dueDateStart: getTodayAtHour(17),
|
|
64
|
+
dueDateEnd: getTodayAtHour(17),
|
|
65
|
+
dueTimeStart: getTodayTimeAtHour(17),
|
|
66
|
+
dueTimeEnd: getTodayTimeAtHour(17, 30),
|
|
55
67
|
isCompleted: true,
|
|
56
68
|
color: '#8b5cf6',
|
|
57
69
|
},
|
|
@@ -65,10 +77,18 @@ export const Interactive = {
|
|
|
65
77
|
console.log(`Task ${taskId} dropped at ${hour}:${minutes}`);
|
|
66
78
|
setTasks((prev) => prev.map((t) => {
|
|
67
79
|
if (t.id === taskId) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const dtStart = t.dueDateStart.toPlainDateTime(t.dueTimeStart || Temporal.PlainTime.from('00:00'));
|
|
81
|
+
const dtEnd = (t.dueDateEnd || t.dueDateStart).toPlainDateTime(t.dueTimeEnd || Temporal.PlainTime.from('23:59'));
|
|
82
|
+
const duration = dtEnd.since(dtStart);
|
|
83
|
+
const newDtStart = t.dueDateStart.toPlainDateTime(Temporal.PlainTime.from({ hour, minute: minutes }));
|
|
84
|
+
const newDtEnd = newDtStart.add(duration);
|
|
85
|
+
return {
|
|
86
|
+
...t,
|
|
87
|
+
dueDateStart: newDtStart.toPlainDate(),
|
|
88
|
+
dueDateEnd: newDtEnd.toPlainDate(),
|
|
89
|
+
dueTimeStart: newDtStart.toPlainTime(),
|
|
90
|
+
dueTimeEnd: newDtEnd.toPlainTime(),
|
|
91
|
+
};
|
|
72
92
|
}
|
|
73
93
|
return t;
|
|
74
94
|
}));
|
|
@@ -77,24 +97,36 @@ export const Interactive = {
|
|
|
77
97
|
console.log(`Task ${taskId} resized to ${newEstimatedTime} minutes. New start: ${newStartHour}:${newStartMinutes}`);
|
|
78
98
|
setTasks((prev) => prev.map((t) => {
|
|
79
99
|
if (t.id === taskId) {
|
|
80
|
-
|
|
100
|
+
let newDtStart;
|
|
81
101
|
if (newStartHour !== undefined && newStartMinutes !== undefined) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
newDtStart = t.dueDateStart.toPlainDateTime(Temporal.PlainTime.from({ hour: newStartHour, minute: newStartMinutes }));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
newDtStart = t.dueDateStart.toPlainDateTime(t.dueTimeStart || Temporal.PlainTime.from('00:00'));
|
|
85
106
|
}
|
|
86
|
-
|
|
107
|
+
const newDtEnd = newDtStart.add({ minutes: newEstimatedTime });
|
|
108
|
+
return {
|
|
109
|
+
...t,
|
|
110
|
+
dueDateStart: newDtStart.toPlainDate(),
|
|
111
|
+
dueDateEnd: newDtEnd.toPlainDate(),
|
|
112
|
+
dueTimeStart: newDtStart.toPlainTime(),
|
|
113
|
+
dueTimeEnd: newDtEnd.toPlainTime(),
|
|
114
|
+
};
|
|
87
115
|
}
|
|
88
116
|
return t;
|
|
89
117
|
}));
|
|
90
118
|
};
|
|
91
119
|
const handleCreateTask = (hour, minutes, estimatedTime) => {
|
|
92
120
|
console.log(`Create task at ${hour}:${minutes} with duration ${estimatedTime}`);
|
|
121
|
+
const dtStart = Temporal.Now.plainDateISO().toPlainDateTime(Temporal.PlainTime.from({ hour, minute: minutes }));
|
|
122
|
+
const dtEnd = dtStart.add({ minutes: estimatedTime });
|
|
93
123
|
const newTask = {
|
|
94
124
|
id: Math.random().toString(36).substr(2, 9),
|
|
95
125
|
title: 'Новая задача',
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
dueDateStart: dtStart.toPlainDate(),
|
|
127
|
+
dueDateEnd: dtEnd.toPlainDate(),
|
|
128
|
+
dueTimeStart: dtStart.toPlainTime(),
|
|
129
|
+
dueTimeEnd: dtEnd.toPlainTime(),
|
|
98
130
|
color: '#8b5cf6',
|
|
99
131
|
};
|
|
100
132
|
setTasks((prev) => [...prev, newTask]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
3
|
import { animated, config, useSpring } from '@react-spring/web';
|
|
3
4
|
import { useState } from 'react';
|
|
4
5
|
import { jc } from '../../../utils';
|
|
@@ -12,7 +13,41 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
12
13
|
const springs = useSpring({
|
|
13
14
|
to: {
|
|
14
15
|
opacity: dragOverInfo ? 1 : 0.6,
|
|
15
|
-
height: dragOverInfo
|
|
16
|
+
height: dragOverInfo
|
|
17
|
+
? (() => {
|
|
18
|
+
const toISO = (d) => {
|
|
19
|
+
if (d instanceof Temporal.PlainDate ||
|
|
20
|
+
d instanceof Temporal.PlainDateTime ||
|
|
21
|
+
d instanceof Temporal.Instant)
|
|
22
|
+
return d.toString().split('T')[0];
|
|
23
|
+
if (d instanceof Date)
|
|
24
|
+
return d.toISOString().split('T')[0];
|
|
25
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
26
|
+
const parsed = new Date(d);
|
|
27
|
+
if (!isNaN(parsed.getTime()))
|
|
28
|
+
return parsed.toISOString().split('T')[0];
|
|
29
|
+
}
|
|
30
|
+
return String(d);
|
|
31
|
+
};
|
|
32
|
+
const task = dragOverInfo.task;
|
|
33
|
+
const dStart = task.dueDateStart instanceof Temporal.PlainDate
|
|
34
|
+
? task.dueDateStart
|
|
35
|
+
: Temporal.PlainDate.from(toISO(task.dueDateStart));
|
|
36
|
+
const dEnd = task.dueDateEnd instanceof Temporal.PlainDate
|
|
37
|
+
? task.dueDateEnd
|
|
38
|
+
: Temporal.PlainDate.from(toISO(task.dueDateEnd));
|
|
39
|
+
const tStart = task.dueTimeStart instanceof Temporal.PlainTime
|
|
40
|
+
? task.dueTimeStart
|
|
41
|
+
: Temporal.PlainTime.from(String(task.dueTimeStart || '00:00'));
|
|
42
|
+
const tEnd = task.dueTimeEnd instanceof Temporal.PlainTime
|
|
43
|
+
? task.dueTimeEnd
|
|
44
|
+
: Temporal.PlainTime.from(String(task.dueTimeEnd || '23:59'));
|
|
45
|
+
const dtStart = dStart.toPlainDateTime(tStart);
|
|
46
|
+
const dtEnd = dEnd.toPlainDateTime(tEnd);
|
|
47
|
+
const minutes = Number(dtEnd.since(dtStart).total('minute'));
|
|
48
|
+
return `${(minutes / 15) * 20}px`;
|
|
49
|
+
})()
|
|
50
|
+
: '40px',
|
|
16
51
|
},
|
|
17
52
|
config: { ...config.stiff, precision: 0.001 },
|
|
18
53
|
});
|
|
@@ -81,8 +116,12 @@ export const CalendarSlot = ({ slotIndex, onDrop, onCreateTask, renderTask, chil
|
|
|
81
116
|
}, children: renderTask ? (renderTask({
|
|
82
117
|
id: 'new-task-preview',
|
|
83
118
|
title: 'Новая задача',
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
dueDateStart: Temporal.Now.plainDateISO(),
|
|
120
|
+
dueTimeStart: Temporal.PlainTime.from({ hour, minute: minutes }),
|
|
121
|
+
dueDateEnd: Temporal.Now.plainDateISO(),
|
|
122
|
+
dueTimeEnd: Temporal.PlainTime.from({ hour, minute: minutes }).add({
|
|
123
|
+
minutes: selection.currentEstimatedTime,
|
|
124
|
+
}),
|
|
86
125
|
color: 'white',
|
|
87
126
|
})) : (_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"] })] })) }))] })] }));
|
|
88
127
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { ReactNode } from 'react';
|
|
2
3
|
export type TCalendarTask = {
|
|
3
4
|
id: string;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
dueDateStart: Temporal.PlainDate;
|
|
6
|
+
dueDateEnd: Temporal.PlainDate;
|
|
6
7
|
[key: string]: any;
|
|
7
8
|
};
|
|
8
9
|
export type TCalendarTaskWithPosition = TCalendarTask & {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { useEffect } from 'react';
|
|
2
3
|
export const useAutoScroll = (containerRef, enabled, topOffset) => {
|
|
3
4
|
useEffect(() => {
|
|
@@ -5,9 +6,9 @@ export const useAutoScroll = (containerRef, enabled, topOffset) => {
|
|
|
5
6
|
const scroll = (retryCount = 0) => {
|
|
6
7
|
if (!containerRef.current)
|
|
7
8
|
return;
|
|
8
|
-
const now =
|
|
9
|
-
const hours = now.
|
|
10
|
-
const minutes = now.
|
|
9
|
+
const now = Temporal.Now.plainTimeISO();
|
|
10
|
+
const hours = now.hour;
|
|
11
|
+
const minutes = now.minute;
|
|
11
12
|
const totalMinutes = hours * 60 + minutes;
|
|
12
13
|
// Находим ближайший скроллируемый родитель или используем окно
|
|
13
14
|
const scrollParent = (node) => {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
1
2
|
import { useState, useEffect, useMemo } from 'react';
|
|
2
3
|
export const useCurrentTime = (updateInterval = 60000) => {
|
|
3
|
-
const [now, setNow] = useState(
|
|
4
|
+
const [now, setNow] = useState(Temporal.Now.plainTimeISO());
|
|
4
5
|
useEffect(() => {
|
|
5
6
|
const interval = setInterval(() => {
|
|
6
|
-
setNow(
|
|
7
|
+
setNow(Temporal.Now.plainTimeISO());
|
|
7
8
|
}, updateInterval);
|
|
8
9
|
return () => clearInterval(interval);
|
|
9
10
|
}, [updateInterval]);
|
|
10
11
|
const topOffset = useMemo(() => {
|
|
11
|
-
const hours = now.
|
|
12
|
-
const minutes = now.
|
|
12
|
+
const hours = now.hour;
|
|
13
|
+
const minutes = now.minute;
|
|
13
14
|
const totalMinutes = hours * 60 + minutes;
|
|
14
15
|
// 15 минут = 20 пикселей (высота слота)
|
|
15
16
|
return totalMinutes * (20 / 15);
|
|
@@ -1,19 +1,39 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
const toDateTime = (date, time) => {
|
|
3
|
+
const toISO = (d) => {
|
|
4
|
+
if (d instanceof Temporal.PlainDate || d instanceof Temporal.PlainDateTime || d instanceof Temporal.Instant)
|
|
5
|
+
return d.toString().split('T')[0];
|
|
6
|
+
if (d instanceof Date)
|
|
7
|
+
return d.toISOString().split('T')[0];
|
|
8
|
+
if (typeof d === 'string' && d.includes(' ')) {
|
|
9
|
+
const parsed = new Date(d);
|
|
10
|
+
if (!isNaN(parsed.getTime()))
|
|
11
|
+
return parsed.toISOString().split('T')[0];
|
|
12
|
+
}
|
|
13
|
+
return String(d);
|
|
14
|
+
};
|
|
15
|
+
const d = date instanceof Temporal.PlainDate ? date : Temporal.PlainDate.from(toISO(date));
|
|
16
|
+
const t = time instanceof Temporal.PlainTime ? time : Temporal.PlainTime.from(String(time || '00:00'));
|
|
17
|
+
return d.toPlainDateTime(t);
|
|
18
|
+
};
|
|
1
19
|
export const calculateTaskPositions = (tasks) => {
|
|
2
20
|
const tasksToRender = [...tasks]
|
|
3
|
-
.map((t) =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
.map((t) => {
|
|
22
|
+
const dtStart = toDateTime(t.dueDateStart, t.dueTimeStart);
|
|
23
|
+
const dtEnd = toDateTime(t.dueDateEnd, t.dueTimeEnd);
|
|
24
|
+
return {
|
|
25
|
+
...t,
|
|
26
|
+
dtStart,
|
|
27
|
+
dtEnd,
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
.sort((a, b) => Temporal.PlainDateTime.compare(a.dtStart, b.dtStart));
|
|
9
31
|
const columns = [];
|
|
10
32
|
tasksToRender.forEach((task) => {
|
|
11
|
-
const startTime = task.dueDate.getTime();
|
|
12
33
|
let placed = false;
|
|
13
34
|
for (const column of columns) {
|
|
14
35
|
const lastTask = column[column.length - 1];
|
|
15
|
-
|
|
16
|
-
if (startTime >= lastEndTime) {
|
|
36
|
+
if (Temporal.PlainDateTime.compare(task.dtStart, lastTask.dtEnd) >= 0) {
|
|
17
37
|
column.push(task);
|
|
18
38
|
placed = true;
|
|
19
39
|
break;
|
|
@@ -25,24 +45,27 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
25
45
|
});
|
|
26
46
|
const clusters = [];
|
|
27
47
|
let currentCluster = [];
|
|
28
|
-
let clusterMaxEndTime =
|
|
48
|
+
let clusterMaxEndTime = null;
|
|
29
49
|
columns.forEach((column) => {
|
|
30
|
-
let columnMinStartTime =
|
|
50
|
+
let columnMinStartTime = null;
|
|
31
51
|
column.forEach((t) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
if (!columnMinStartTime || Temporal.PlainDateTime.compare(t.dtStart, columnMinStartTime) < 0) {
|
|
53
|
+
columnMinStartTime = t.dtStart;
|
|
54
|
+
}
|
|
35
55
|
});
|
|
36
|
-
if (
|
|
56
|
+
if (clusterMaxEndTime &&
|
|
57
|
+
columnMinStartTime &&
|
|
58
|
+
Temporal.PlainDateTime.compare(columnMinStartTime, clusterMaxEndTime) >= 0 &&
|
|
59
|
+
currentCluster.length > 0) {
|
|
37
60
|
clusters.push(currentCluster);
|
|
38
61
|
currentCluster = [];
|
|
39
|
-
clusterMaxEndTime =
|
|
62
|
+
clusterMaxEndTime = null;
|
|
40
63
|
}
|
|
41
64
|
currentCluster.push(column);
|
|
42
65
|
column.forEach((t) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
if (!clusterMaxEndTime || Temporal.PlainDateTime.compare(t.dtEnd, clusterMaxEndTime) > 0) {
|
|
67
|
+
clusterMaxEndTime = t.dtEnd;
|
|
68
|
+
}
|
|
46
69
|
});
|
|
47
70
|
});
|
|
48
71
|
if (currentCluster.length > 0)
|
|
@@ -55,11 +78,12 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
55
78
|
const width = clusterColumnsCount > 1 ? `${100 / clusterColumnsCount}%` : '100%';
|
|
56
79
|
const left = clusterColumnsCount > 1 ? `${(colIndex * 100) / clusterColumnsCount}%` : '0%';
|
|
57
80
|
// Расчитываем смещение внутри 15-минутного слота
|
|
58
|
-
const minutes = task.
|
|
81
|
+
const minutes = task.dtStart.minute;
|
|
59
82
|
const minutesInSlot = minutes % 15;
|
|
60
83
|
const topOffset = (minutesInSlot / 15) * 20;
|
|
61
|
-
const top = `${Math.round(topOffset)}px`;
|
|
62
|
-
const
|
|
84
|
+
const top = `${Math.round(topOffset)}px`;
|
|
85
|
+
const diffMinutes = Math.round(Number(task.dtEnd.since(task.dtStart).total('minute')));
|
|
86
|
+
const height = `${Math.round((Math.max(15, diffMinutes || 15) / 15) * 20)}px`;
|
|
63
87
|
result.push({
|
|
64
88
|
...task,
|
|
65
89
|
position: { width, left, top, height },
|
|
@@ -72,8 +96,9 @@ export const calculateTaskPositions = (tasks) => {
|
|
|
72
96
|
export const groupTasksBySlot = (tasksWithPosition) => {
|
|
73
97
|
const map = {};
|
|
74
98
|
tasksWithPosition.forEach((task) => {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
99
|
+
const dtStart = toDateTime(task.dueDateStart, task.dueTimeStart);
|
|
100
|
+
const hour = dtStart.hour;
|
|
101
|
+
const minutes = dtStart.minute;
|
|
77
102
|
const slotIndex = hour * 4 + Math.floor(minutes / 15);
|
|
78
103
|
if (!map[slotIndex])
|
|
79
104
|
map[slotIndex] = [];
|
|
@@ -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, { direction: 'column', justify: 'center', gap: '16px', type: 'fill', className: jc(expandedPanel ? 'hidden @s:flex' : 'flex', 'h-full mt-[50px] @s:m-0', '@s:flex-row @s:items-stretch'), children: [!widget?.isFullWidth && (_jsx("div", { className: jc('h-
|
|
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, { direction: 'column', justify: 'center', gap: '16px', type: 'fill', className: jc(expandedPanel ? 'hidden @s:flex' : 'flex', 'h-full mt-[50px] @s:m-0', '@s:flex-row @s:items-stretch'), children: [!widget?.isFullWidth && (_jsx("div", { className: jc('min-h-0 h-full scrollbar-none overflow-auto', '@s:flex-1', widget ? 'hidden @s:block' : 'block'), children: children })), widget && (_jsx("div", { className: jc('flex-1 min-h-0 h-full w-full', !widget.isFullWidth && '@s:flex-initial @s:w-[360px]'), children: _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.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "UI Kit for kkkarsss projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"@storybook/react": "^10.2.7",
|
|
34
34
|
"@storybook/react-vite": "^10.2.7",
|
|
35
35
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
36
|
-
"tailwindcss": "^3.4.19",
|
|
37
36
|
"@types/node": "^25.0.10",
|
|
38
37
|
"@types/react": "^19.2.9",
|
|
39
38
|
"@types/react-dom": "^19.2.3",
|
|
@@ -55,12 +54,14 @@
|
|
|
55
54
|
"react": "^19.2.3",
|
|
56
55
|
"react-dom": "^19.2.3",
|
|
57
56
|
"storybook": "^10.2.7",
|
|
57
|
+
"tailwindcss": "^3.4.19",
|
|
58
58
|
"typescript": "5.9.3",
|
|
59
59
|
"typescript-eslint": "^8.53.1",
|
|
60
60
|
"vite": "^7.3.1",
|
|
61
61
|
"vitest": "^4.0.18"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
+
"@js-temporal/polyfill": "^0.5.1",
|
|
64
65
|
"@react-spring/web": "^10.0.3",
|
|
65
66
|
"date-fns": "^4.1.0",
|
|
66
67
|
"react-datepicker": "^9.1.0",
|