@koslibs/components 1.15.0 → 1.16.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/CHANGELOG.md +8 -0
- package/dist/components/Datepicker/components/calendar-month.d.ts +8 -0
- package/dist/components/Datepicker/components/calendar-month.js +68 -0
- package/dist/components/Datepicker/components/range-calendar.d.ts +5 -0
- package/dist/components/Datepicker/components/range-calendar.js +78 -0
- package/dist/components/Datepicker/datepicker-panel.d.ts +2 -0
- package/dist/components/Datepicker/datepicker-panel.js +187 -0
- package/dist/components/Datepicker/datepicker.d.ts +2 -0
- package/dist/components/Datepicker/datepicker.js +194 -0
- package/dist/components/Datepicker/hooks/use-datepicker-draft.d.ts +22 -0
- package/dist/components/Datepicker/hooks/use-datepicker-draft.js +105 -0
- package/dist/components/Datepicker/hooks/use-datepicker-state.d.ts +23 -0
- package/dist/components/Datepicker/hooks/use-datepicker-state.js +106 -0
- package/dist/components/Datepicker/hooks/use-datepicker-value.d.ts +8 -0
- package/dist/components/Datepicker/hooks/use-datepicker-value.js +31 -0
- package/dist/components/Datepicker/hooks/use-range-scroll.d.ts +15 -0
- package/dist/components/Datepicker/hooks/use-range-scroll.js +219 -0
- package/dist/components/Datepicker/index.d.ts +3 -0
- package/dist/components/Datepicker/index.js +2 -0
- package/dist/components/Datepicker/index.module.js +37 -0
- package/dist/components/Datepicker/index_module.css +251 -0
- package/dist/components/Datepicker/panel.module.js +39 -0
- package/dist/components/Datepicker/panel_module.css +375 -0
- package/dist/components/Datepicker/types.d.ts +133 -0
- package/dist/components/Datepicker/types.js +0 -0
- package/dist/components/Datepicker/utils/date-utils.d.ts +17 -0
- package/dist/components/Datepicker/utils/date-utils.js +80 -0
- package/dist/components/Datepicker/utils/range-scroll-geometry.d.ts +15 -0
- package/dist/components/Datepicker/utils/range-scroll-geometry.js +29 -0
- package/dist/components/Datepicker/vars.css +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, CSSProperties, MouseEvent } from 'react';
|
|
2
|
+
import type { InputProps } from '../Input/index.js';
|
|
3
|
+
import type { PopoverProps } from '../Popover/index.js';
|
|
4
|
+
export type DatepickerRange = {
|
|
5
|
+
/**
|
|
6
|
+
* Начало включительного диапазона в формате YYYY-MM-DD.
|
|
7
|
+
*/
|
|
8
|
+
from: string;
|
|
9
|
+
/**
|
|
10
|
+
* Конец включительного диапазона в формате YYYY-MM-DD.
|
|
11
|
+
*/
|
|
12
|
+
to: string;
|
|
13
|
+
};
|
|
14
|
+
export type DatepickerChangePayload<Value = string | null> = {
|
|
15
|
+
/**
|
|
16
|
+
* Применённая дата или полный диапазон; null означает пустое значение.
|
|
17
|
+
*/
|
|
18
|
+
value: Value;
|
|
19
|
+
};
|
|
20
|
+
type SingleProps = {
|
|
21
|
+
/**
|
|
22
|
+
* Выбор одной даты.
|
|
23
|
+
* @default 'single'
|
|
24
|
+
*/
|
|
25
|
+
mode?: 'single';
|
|
26
|
+
/**
|
|
27
|
+
* Управляемая дата YYYY-MM-DD или null.
|
|
28
|
+
*/
|
|
29
|
+
value?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Начальная дата неуправляемого компонента.
|
|
32
|
+
*/
|
|
33
|
+
defaultValue?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Подтверждение по Apply. Клики дней и ручной ввод меняют только черновик.
|
|
36
|
+
*/
|
|
37
|
+
onChange?: (event: MouseEvent<HTMLButtonElement>, payload: DatepickerChangePayload) => void;
|
|
38
|
+
};
|
|
39
|
+
type RangeProps = {
|
|
40
|
+
/**
|
|
41
|
+
* Выбор включительного диапазона дат.
|
|
42
|
+
*/
|
|
43
|
+
mode: 'range';
|
|
44
|
+
/**
|
|
45
|
+
* Управляемый полный диапазон YYYY-MM-DD или null.
|
|
46
|
+
*/
|
|
47
|
+
value?: DatepickerRange | null;
|
|
48
|
+
/**
|
|
49
|
+
* Начальный диапазон неуправляемого компонента.
|
|
50
|
+
*/
|
|
51
|
+
defaultValue?: DatepickerRange | null;
|
|
52
|
+
/**
|
|
53
|
+
* Подтверждение полного корректного диапазона по Apply.
|
|
54
|
+
*/
|
|
55
|
+
onChange?: (event: MouseEvent<HTMLButtonElement>, payload: DatepickerChangePayload<DatepickerRange | null>) => void;
|
|
56
|
+
};
|
|
57
|
+
export type DatepickerValueProps = SingleProps | RangeProps;
|
|
58
|
+
export type DatepickerPanelProps = DatepickerValueProps & {
|
|
59
|
+
/**
|
|
60
|
+
* HTML-идентификатор панели.
|
|
61
|
+
*/
|
|
62
|
+
id?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Отмена черновика кнопкой Cancel или Escape. Панель остаётся видимой.
|
|
65
|
+
*/
|
|
66
|
+
onCancel?: VoidFunction;
|
|
67
|
+
/**
|
|
68
|
+
* Класс панели.
|
|
69
|
+
*/
|
|
70
|
+
className?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Стили панели и её CSS-переменные.
|
|
73
|
+
*/
|
|
74
|
+
style?: CSSProperties;
|
|
75
|
+
/**
|
|
76
|
+
* Идентификатор панели. Части: -date/-from/-to, -apply, -cancel,
|
|
77
|
+
* -count, -day-YYYY-MM-DD, -month-N, -months, -month-window, -calendar, -calendar-months,
|
|
78
|
+
* -previous/-next.
|
|
79
|
+
*/
|
|
80
|
+
dataTestId?: string;
|
|
81
|
+
};
|
|
82
|
+
type FieldProps = Pick<InputProps, 'view' | 'size' | 'block' | 'label' | 'labelView' | 'hint' | 'error' | 'hasOutline' | 'clear' | 'labelClassName' | 'messageClassName'> & Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'defaultValue' | 'onChange' | 'children' | 'name' | 'type' | 'style'> & {
|
|
83
|
+
/**
|
|
84
|
+
* Стили обёртки поля и её CSS-переменные.
|
|
85
|
+
*/
|
|
86
|
+
style?: CSSProperties;
|
|
87
|
+
/**
|
|
88
|
+
* Сохраняет отображение и фокус, запрещая раскрытие и очистку.
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
readOnly?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Подпись пустого поля.
|
|
94
|
+
* @default 'Choose date' / 'Choose dates'
|
|
95
|
+
*/
|
|
96
|
+
placeholder?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Управляемая видимость панели. Disabled и readOnly скрывают её.
|
|
99
|
+
*/
|
|
100
|
+
open?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Начальная видимость неуправляемой панели.
|
|
103
|
+
* @default false
|
|
104
|
+
*/
|
|
105
|
+
defaultOpen?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Запрос изменения видимости панели.
|
|
108
|
+
*/
|
|
109
|
+
onOpenChange?: (open: boolean) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Очистка вызывает только onClear. При управляемом value обновите его сами.
|
|
112
|
+
*/
|
|
113
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Имя в форме. Range отправляет name.from и name.to.
|
|
116
|
+
*/
|
|
117
|
+
name?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Класс всплывающей панели, отдельный от класса поля.
|
|
120
|
+
*/
|
|
121
|
+
panelClassName?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Контейнер портала, как в Popover.
|
|
124
|
+
*/
|
|
125
|
+
getPortalContainer?: PopoverProps['getPortalContainer'];
|
|
126
|
+
/**
|
|
127
|
+
* Идентификатор кнопки поля. Части: -form-control, -field, -label,
|
|
128
|
+
* -value, -hint/-error, -clear, -popup, -panel и части панели.
|
|
129
|
+
*/
|
|
130
|
+
dataTestId?: string;
|
|
131
|
+
};
|
|
132
|
+
export type DatepickerProps = FieldProps & DatepickerValueProps;
|
|
133
|
+
export {};
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatepickerRange } from '../types.js';
|
|
2
|
+
export declare const LAST_MONTH: number;
|
|
3
|
+
export declare const MONTH_NAMES: string[];
|
|
4
|
+
export declare function parseDate(value: string): {
|
|
5
|
+
year: number;
|
|
6
|
+
month: number;
|
|
7
|
+
day: number;
|
|
8
|
+
} | null;
|
|
9
|
+
export declare function formatDate(value: string): string;
|
|
10
|
+
export declare function getMonthIndex(value: string): number;
|
|
11
|
+
export declare function monthDate(monthIndex: number, day?: number): string;
|
|
12
|
+
export declare function monthLabel(monthIndex: number): string;
|
|
13
|
+
export declare function clampMonth(monthIndex: number, range?: boolean): number;
|
|
14
|
+
export declare function getMonthDays(monthIndex: number): (string | null)[];
|
|
15
|
+
export declare function countDays(from: string, to: string): number;
|
|
16
|
+
export declare function today(): string;
|
|
17
|
+
export declare function normalizeValue(mode: 'single' | 'range', value: unknown): string | DatepickerRange | null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const LAST_MONTH = 119987;
|
|
2
|
+
const MONTH_NAMES = [
|
|
3
|
+
'January',
|
|
4
|
+
'February',
|
|
5
|
+
'March',
|
|
6
|
+
'April',
|
|
7
|
+
'May',
|
|
8
|
+
'June',
|
|
9
|
+
'July',
|
|
10
|
+
'August',
|
|
11
|
+
'September',
|
|
12
|
+
'October',
|
|
13
|
+
'November',
|
|
14
|
+
'December'
|
|
15
|
+
];
|
|
16
|
+
function daysInMonth(year, month) {
|
|
17
|
+
if (2 === month) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
18
|
+
return [
|
|
19
|
+
4,
|
|
20
|
+
6,
|
|
21
|
+
9,
|
|
22
|
+
11
|
|
23
|
+
].includes(month) ? 30 : 31;
|
|
24
|
+
}
|
|
25
|
+
function parseDate(value) {
|
|
26
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
|
|
27
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
28
|
+
return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month) ? {
|
|
29
|
+
year,
|
|
30
|
+
month,
|
|
31
|
+
day
|
|
32
|
+
} : null;
|
|
33
|
+
}
|
|
34
|
+
function ordinal(value) {
|
|
35
|
+
const { year, month, day } = parseDate(value);
|
|
36
|
+
const date = new Date(0);
|
|
37
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
38
|
+
return date.getTime() / 86400000;
|
|
39
|
+
}
|
|
40
|
+
function formatDate(value) {
|
|
41
|
+
return value.split('-').reverse().join('/');
|
|
42
|
+
}
|
|
43
|
+
function getMonthIndex(value) {
|
|
44
|
+
const { year, month } = parseDate(value);
|
|
45
|
+
return (year - 1) * 12 + month - 1;
|
|
46
|
+
}
|
|
47
|
+
function monthDate(monthIndex, day = 1) {
|
|
48
|
+
return `${String(Math.floor(monthIndex / 12) + 1).padStart(4, '0')}-${String(monthIndex % 12 + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
49
|
+
}
|
|
50
|
+
function monthLabel(monthIndex) {
|
|
51
|
+
return `${MONTH_NAMES[monthIndex % 12]} ${Math.floor(monthIndex / 12) + 1}`;
|
|
52
|
+
}
|
|
53
|
+
function clampMonth(monthIndex, range = false) {
|
|
54
|
+
return Math.min(LAST_MONTH - Number(range), Math.max(0, monthIndex));
|
|
55
|
+
}
|
|
56
|
+
function getMonthDays(monthIndex) {
|
|
57
|
+
const first = monthDate(monthIndex);
|
|
58
|
+
const offset = ((ordinal(first) + 3) % 7 + 7) % 7;
|
|
59
|
+
const length = daysInMonth(Math.floor(monthIndex / 12) + 1, monthIndex % 12 + 1);
|
|
60
|
+
return Array.from({
|
|
61
|
+
length: 7 * Math.ceil((offset + length) / 7)
|
|
62
|
+
}, (_, index)=>index < offset || index >= offset + length ? null : monthDate(monthIndex, index - offset + 1));
|
|
63
|
+
}
|
|
64
|
+
function countDays(from, to) {
|
|
65
|
+
return ordinal(to) - ordinal(from) + 1;
|
|
66
|
+
}
|
|
67
|
+
function today() {
|
|
68
|
+
const date = new Date();
|
|
69
|
+
return `${String(date.getFullYear()).padStart(4, '0')}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
70
|
+
}
|
|
71
|
+
function normalizeValue(mode, value) {
|
|
72
|
+
if ('single' === mode) return 'string' == typeof value && parseDate(value) ? value : null;
|
|
73
|
+
if (!value || 'object' != typeof value) return null;
|
|
74
|
+
const { from, to } = value;
|
|
75
|
+
return 'string' == typeof from && 'string' == typeof to && parseDate(from) && parseDate(to) && from <= to ? {
|
|
76
|
+
from,
|
|
77
|
+
to
|
|
78
|
+
} : null;
|
|
79
|
+
}
|
|
80
|
+
export { LAST_MONTH, MONTH_NAMES, clampMonth, countDays, formatDate, getMonthDays, getMonthIndex, monthDate, monthLabel, normalizeValue, parseDate, today };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type MonthPosition = {
|
|
2
|
+
month: number;
|
|
3
|
+
fraction: number;
|
|
4
|
+
};
|
|
5
|
+
type MonthRow = {
|
|
6
|
+
month: number;
|
|
7
|
+
start: number;
|
|
8
|
+
contentEnd: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
export type MonthGeometry = readonly MonthRow[];
|
|
12
|
+
export declare function measureMonths(elements: ReadonlyMap<number, HTMLElement>): MonthGeometry;
|
|
13
|
+
export declare function positionAt(geometry: MonthGeometry, offset: number): MonthPosition | null;
|
|
14
|
+
export declare function offsetFor(geometry: MonthGeometry, position: MonthPosition): number | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function measureMonths(elements) {
|
|
2
|
+
const measured = Array.from(elements, ([month, element])=>{
|
|
3
|
+
const start = element.offsetTop;
|
|
4
|
+
return {
|
|
5
|
+
month,
|
|
6
|
+
start,
|
|
7
|
+
contentEnd: start + element.offsetHeight
|
|
8
|
+
};
|
|
9
|
+
}).sort((first, second)=>first.month - second.month);
|
|
10
|
+
if (!measured.length || measured[0].contentEnd <= measured[0].start) return [];
|
|
11
|
+
return measured.map((row, index)=>({
|
|
12
|
+
...row,
|
|
13
|
+
end: measured[index + 1]?.start ?? row.contentEnd
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function positionAt(geometry, offset) {
|
|
17
|
+
const index = Math.max(0, geometry.findLastIndex((row)=>row.start <= offset));
|
|
18
|
+
const row = geometry[index];
|
|
19
|
+
if (!row || row.end <= row.start) return null;
|
|
20
|
+
return {
|
|
21
|
+
month: row.month,
|
|
22
|
+
fraction: Math.max(0, Math.min(1, (offset - row.start) / (row.end - row.start)))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function offsetFor(geometry, position) {
|
|
26
|
+
const row = geometry.find((candidate)=>candidate.month === position.month);
|
|
27
|
+
return row ? row.start + position.fraction * (row.end - row.start) : null;
|
|
28
|
+
}
|
|
29
|
+
export { measureMonths, offsetFor, positionAt };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "../Input/vars.css";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--datepicker-width: 320px;
|
|
5
|
+
--datepicker-panel-width: 360px;
|
|
6
|
+
--datepicker-range-width: 560px;
|
|
7
|
+
--datepicker-panel-padding: var(--size-20);
|
|
8
|
+
--datepicker-panel-radius: var(--border-radius-12);
|
|
9
|
+
--datepicker-panel-shadow: 0 12px 32px -8px #0003;
|
|
10
|
+
--datepicker-background: #fff;
|
|
11
|
+
--datepicker-color: var(--input-color);
|
|
12
|
+
--datepicker-muted-color: var(--input-label-color);
|
|
13
|
+
--datepicker-hover-background: #f5f3ec;
|
|
14
|
+
--datepicker-selected-background: #fdde9b;
|
|
15
|
+
--datepicker-range-background: #f5f3ec;
|
|
16
|
+
--datepicker-month-selected-background: #e0dbce;
|
|
17
|
+
--datepicker-focus-color: var(--input-focus-border-color);
|
|
18
|
+
--datepicker-scrollbar-color: #c8beaa;
|
|
19
|
+
--datepicker-day-height: 32px;
|
|
20
|
+
--datepicker-day-radius: var(--border-radius-8);
|
|
21
|
+
--datepicker-week-gap: var(--size-4);
|
|
22
|
+
--datepicker-months-width: 168px;
|
|
23
|
+
--datepicker-months-extra-height: 36px;
|
|
24
|
+
--datepicker-range-calendar-height: 420px;
|
|
25
|
+
}
|
|
26
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { Button } from './components/Button/index.js';
|
|
|
2
2
|
export type { ButtonProps } from './components/Button/index.js';
|
|
3
3
|
export { Input } from './components/Input/index.js';
|
|
4
4
|
export type { InputProps } from './components/Input/index.js';
|
|
5
|
+
export { Datepicker, DatepickerPanel } from './components/Datepicker/index.js';
|
|
6
|
+
export type { DatepickerProps, DatepickerPanelProps, DatepickerRange, DatepickerChangePayload, } from './components/Datepicker/index.js';
|
|
5
7
|
export { Select } from './components/Select/index.js';
|
|
6
8
|
export type { SelectProps, SelectOption, SelectChangeEvent, SelectChangePayload, } from './components/Select/index.js';
|
|
7
9
|
export { Textarea } from './components/Textarea/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Button } from "./components/Button/index.js";
|
|
2
2
|
export { Input } from "./components/Input/index.js";
|
|
3
|
+
export { Datepicker, DatepickerPanel } from "./components/Datepicker/index.js";
|
|
3
4
|
export { Select } from "./components/Select/index.js";
|
|
4
5
|
export { Textarea } from "./components/Textarea/index.js";
|
|
5
6
|
export { Tab, Tabs } from "./components/Tabs/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koslibs/components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Shared React components for koslibs projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"react-dom": "^18.3.1 || ^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@koslibs/icons": "1.
|
|
84
|
+
"@koslibs/icons": "1.3.0",
|
|
85
85
|
"@popperjs/core": "2.11.8",
|
|
86
86
|
"classnames": "2.5.1"
|
|
87
87
|
}
|