@de-kit/st-ui 0.1.16 → 0.1.18
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/components/{birthday-picker/birthday-picker.cjs → date-picker/date-picker.cjs} +27 -27
- package/dist/components/date-picker/date-picker.cjs.map +1 -0
- package/dist/components/{birthday-picker/birthday-picker.d.ts → date-picker/date-picker.d.cts} +8 -5
- package/dist/components/{birthday-picker/birthday-picker.d.cts → date-picker/date-picker.d.ts} +8 -5
- package/dist/components/{birthday-picker/birthday-picker.js → date-picker/date-picker.js} +27 -27
- package/dist/components/date-picker/date-picker.js.map +1 -0
- package/dist/components/{birthday-picker/birthday-picker.scss → date-picker/date-picker.scss} +11 -11
- package/dist/components/date-picker/index.d.cts +2 -0
- package/dist/components/date-picker/index.d.ts +2 -0
- package/dist/components/date-picker/wheel-column.cjs.map +1 -0
- package/dist/components/date-picker/wheel-column.js.map +1 -0
- package/dist/components/field/field.scss +3 -3
- package/dist/components/input/input.scss +3 -2
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/styles/global.scss +1 -32
- package/dist/styles/mixins.scss +46 -0
- package/dist/styles/typography.scss +70 -0
- package/dist/styles/vars.scss +15 -6
- package/package.json +1 -1
- package/dist/components/birthday-picker/birthday-picker.cjs.map +0 -1
- package/dist/components/birthday-picker/birthday-picker.js.map +0 -1
- package/dist/components/birthday-picker/index.d.cts +0 -2
- package/dist/components/birthday-picker/index.d.ts +0 -2
- package/dist/components/birthday-picker/wheel-column.cjs.map +0 -1
- package/dist/components/birthday-picker/wheel-column.js.map +0 -1
- /package/dist/components/{birthday-picker → date-picker}/wheel-column.cjs +0 -0
- /package/dist/components/{birthday-picker → date-picker}/wheel-column.js +0 -0
- /package/dist/components/{birthday-picker → date-picker}/wheel-column.scss +0 -0
|
@@ -8,8 +8,8 @@ let react = require("react");
|
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
let react_dom = require("react-dom");
|
|
10
10
|
require("../input/input.scss");
|
|
11
|
-
require("./
|
|
12
|
-
// src/components/
|
|
11
|
+
require("./date-picker.scss");
|
|
12
|
+
// src/components/date-picker/date-picker.tsx
|
|
13
13
|
const MONTH_NAMES = [
|
|
14
14
|
"январь",
|
|
15
15
|
"февраль",
|
|
@@ -47,9 +47,9 @@ const partsOf = (date) => ({
|
|
|
47
47
|
const toDate = ({ day, month, year }) => new Date(year, month - 1, day);
|
|
48
48
|
const pad = (value) => String(value).padStart(2, "0");
|
|
49
49
|
const toIsoDate = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
50
|
-
const
|
|
50
|
+
const formatDate = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
|
|
51
51
|
/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */
|
|
52
|
-
const
|
|
52
|
+
const maxAllowedDate = (minAge, today) => {
|
|
53
53
|
const year = today.getFullYear() - minAge;
|
|
54
54
|
const month = today.getMonth();
|
|
55
55
|
const day = Math.min(today.getDate(), daysInMonth(year, month + 1));
|
|
@@ -65,14 +65,14 @@ const clampParts = (parts, max) => {
|
|
|
65
65
|
};
|
|
66
66
|
const range = (from, to) => from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.
|
|
69
69
|
* Поле только для чтения с клавиатуры, поэтому `required` из `Field` и
|
|
70
70
|
* нативная проверка формы работают, а телефонная клавиатура не открывается.
|
|
71
71
|
*/
|
|
72
|
-
const
|
|
72
|
+
const DatePicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 0, disabled = false, name, placeholder = "Выберите дату", title = "Выберите дату", error, className }) => {
|
|
73
73
|
const [today] = (0, react.useState)(() => /* @__PURE__ */ new Date());
|
|
74
74
|
const [open, setOpen] = (0, react.useState)(false);
|
|
75
|
-
const maxBirth =
|
|
75
|
+
const maxBirth = maxAllowedDate(minAge, today);
|
|
76
76
|
const lastYear = Math.min(maxYear ?? today.getFullYear(), maxBirth.getFullYear());
|
|
77
77
|
const [draft, setDraft] = (0, react.useState)(() => clampParts(partsOf(value ?? maxBirth), maxBirth));
|
|
78
78
|
const triggerRef = (0, react.useRef)(null);
|
|
@@ -127,12 +127,12 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
127
127
|
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") openSheet();
|
|
128
128
|
};
|
|
129
129
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
130
|
-
className: require_cn.cn("
|
|
130
|
+
className: require_cn.cn("date-picker", className),
|
|
131
131
|
children: [
|
|
132
132
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_input.Input, {
|
|
133
133
|
ref: triggerRef,
|
|
134
|
-
className: "
|
|
135
|
-
value: value ?
|
|
134
|
+
className: "date-picker__trigger",
|
|
135
|
+
value: value ? formatDate(value) : "",
|
|
136
136
|
onChange: () => {},
|
|
137
137
|
onClick: openSheet,
|
|
138
138
|
onKeyDown: handleTriggerKeyDown,
|
|
@@ -141,12 +141,12 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
141
141
|
autoComplete: "off",
|
|
142
142
|
"aria-haspopup": "dialog",
|
|
143
143
|
"aria-expanded": open,
|
|
144
|
-
placeholder
|
|
144
|
+
placeholder,
|
|
145
145
|
disabled,
|
|
146
146
|
error
|
|
147
147
|
}),
|
|
148
148
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_calendar.IconCalendar, {
|
|
149
|
-
className: "
|
|
149
|
+
className: "date-picker__icon",
|
|
150
150
|
"aria-hidden": true
|
|
151
151
|
}),
|
|
152
152
|
name ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -155,10 +155,10 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
155
155
|
value: value ? toIsoDate(value) : ""
|
|
156
156
|
}) : null,
|
|
157
157
|
open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
158
|
-
className: "
|
|
158
|
+
className: "date-picker__layer",
|
|
159
159
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
160
160
|
type: "button",
|
|
161
|
-
className: "
|
|
161
|
+
className: "date-picker__mask",
|
|
162
162
|
"aria-label": "Закрыть",
|
|
163
163
|
onClick: close
|
|
164
164
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
@@ -166,20 +166,20 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
166
166
|
open: true,
|
|
167
167
|
"aria-modal": "true",
|
|
168
168
|
"aria-labelledby": titleId,
|
|
169
|
-
className: "
|
|
169
|
+
className: "date-picker__sheet",
|
|
170
170
|
children: [
|
|
171
171
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
172
172
|
id: titleId,
|
|
173
|
-
className: "
|
|
174
|
-
children:
|
|
173
|
+
className: "date-picker__title",
|
|
174
|
+
children: title
|
|
175
175
|
}),
|
|
176
176
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
177
|
-
className: "
|
|
177
|
+
className: "date-picker__columns",
|
|
178
178
|
children: [
|
|
179
179
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
180
|
-
className: "
|
|
180
|
+
className: "date-picker__column",
|
|
181
181
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
182
|
-
className: "
|
|
182
|
+
className: "date-picker__caption",
|
|
183
183
|
children: "День"
|
|
184
184
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_wheel_column.WheelColumn, {
|
|
185
185
|
label: "День",
|
|
@@ -192,9 +192,9 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
192
192
|
})]
|
|
193
193
|
}),
|
|
194
194
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
195
|
-
className: "
|
|
195
|
+
className: "date-picker__column",
|
|
196
196
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
197
|
-
className: "
|
|
197
|
+
className: "date-picker__caption",
|
|
198
198
|
children: "Месяц"
|
|
199
199
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_wheel_column.WheelColumn, {
|
|
200
200
|
label: "Месяц",
|
|
@@ -207,9 +207,9 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
207
207
|
})]
|
|
208
208
|
}),
|
|
209
209
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
210
|
-
className: "
|
|
210
|
+
className: "date-picker__column",
|
|
211
211
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
212
|
-
className: "
|
|
212
|
+
className: "date-picker__caption",
|
|
213
213
|
children: "Год"
|
|
214
214
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_wheel_column.WheelColumn, {
|
|
215
215
|
label: "Год",
|
|
@@ -224,7 +224,7 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
224
224
|
]
|
|
225
225
|
}),
|
|
226
226
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
227
|
-
className: "
|
|
227
|
+
className: "date-picker__actions",
|
|
228
228
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_button.Button, {
|
|
229
229
|
onClick: apply,
|
|
230
230
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_correct.IconCorrect, {}), "Применить"]
|
|
@@ -240,6 +240,6 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
240
240
|
]
|
|
241
241
|
});
|
|
242
242
|
};
|
|
243
|
-
exports.
|
|
243
|
+
exports.DatePicker = DatePicker;
|
|
244
244
|
|
|
245
|
-
//# sourceMappingURL=
|
|
245
|
+
//# sourceMappingURL=date-picker.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-picker.cjs","names":["useState","useRef","useId","cn","Input","IconCalendar","createPortal","WheelColumn","Button","IconCorrect"],"sources":["../../../src/components/date-picker/date-picker.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Button } from '@/components/button';\nimport { Input } from '@/components/input';\nimport '../input/input.scss';\nimport { IconCalendar } from '@/icons/icon-calendar';\nimport { IconCorrect } from '@/icons/icon-correct';\nimport { cn } from '@/lib/cn';\nimport { WheelColumn } from './wheel-column';\nimport './date-picker.scss';\n\nconst MONTH_NAMES = [\n 'январь',\n 'февраль',\n 'март',\n 'апрель',\n 'май',\n 'июнь',\n 'июль',\n 'август',\n 'сентябрь',\n 'октябрь',\n 'ноябрь',\n 'декабрь',\n];\n\n// В дате пишется родительный падеж: «15 марта 1990».\nconst MONTH_NAMES_GENITIVE = [\n 'января',\n 'февраля',\n 'марта',\n 'апреля',\n 'мая',\n 'июня',\n 'июля',\n 'августа',\n 'сентября',\n 'октября',\n 'ноября',\n 'декабря',\n];\n\n/** Месяц здесь 1–12, как в календаре, а не индекс массива. */\ninterface DateParts {\n day: number;\n month: number;\n year: number;\n}\n\nconst daysInMonth = (year: number, month: number) =>\n new Date(year, month, 0).getDate();\n\nconst partsOf = (date: Date): DateParts => ({\n day: date.getDate(),\n month: date.getMonth() + 1,\n year: date.getFullYear(),\n});\n\nconst toDate = ({ day, month, year }: DateParts) =>\n new Date(year, month - 1, day);\n\nconst pad = (value: number) => String(value).padStart(2, '0');\n\nconst toIsoDate = (date: Date) =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\nconst formatDate = (date: Date) =>\n `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;\n\n/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */\nconst maxAllowedDate = (minAge: number, today: Date) => {\n const year = today.getFullYear() - minAge;\n const month = today.getMonth();\n const day = Math.min(today.getDate(), daysInMonth(year, month + 1));\n return new Date(year, month, day);\n};\n\n/** Подрезает день под длину месяца и не даёт выбрать дату позже максимальной. */\nconst clampParts = (parts: DateParts, max: Date): DateParts => {\n const next = {\n ...parts,\n day: Math.min(parts.day, daysInMonth(parts.year, parts.month)),\n };\n\n return toDate(next) > max ? partsOf(max) : next;\n};\n\nconst range = (from: number, to: number) =>\n from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);\n\nexport interface DatePickerProps {\n value?: Date | null;\n onChange?: (date: Date) => void;\n /** Самый ранний год в списке. */\n minYear?: number;\n /** Самый поздний год в списке. По умолчанию — текущий. */\n maxYear?: number;\n /** Минимальный возраст: позже сегодня минус столько лет выбрать нельзя. 0 — без ограничения. */\n minAge?: number;\n disabled?: boolean;\n /** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */\n name?: string;\n /** Текст в поле, пока дата не выбрана. */\n placeholder?: string;\n /** Заголовок окна выбора. */\n title?: string;\n error?: boolean;\n className?: string;\n}\n\n/**\n * Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.\n * Поле только для чтения с клавиатуры, поэтому `required` из `Field` и\n * нативная проверка формы работают, а телефонная клавиатура не открывается.\n */\nexport const DatePicker = ({\n value,\n onChange,\n minYear = 1930,\n maxYear,\n minAge = 0,\n disabled = false,\n name,\n placeholder = 'Выберите дату',\n title = 'Выберите дату',\n error,\n className,\n}: DatePickerProps) => {\n const [today] = useState(() => new Date());\n const [open, setOpen] = useState(false);\n const maxBirth = maxAllowedDate(minAge, today);\n const lastYear = Math.min(\n maxYear ?? today.getFullYear(),\n maxBirth.getFullYear(),\n );\n const [draft, setDraft] = useState<DateParts>(() =>\n clampParts(partsOf(value ?? maxBirth), maxBirth),\n );\n const triggerRef = useRef<HTMLInputElement>(null);\n const sheetRef = useRef<HTMLDialogElement>(null);\n const titleId = useId();\n\n const years = range(Math.min(minYear, draft.year), lastYear);\n const days = range(1, daysInMonth(draft.year, draft.month));\n\n // iOS: без position: fixed фон прокручивается под открытым листом.\n useEffect(() => {\n if (!open) return;\n\n const { position, top, width } = document.body.style;\n const scrollY = window.scrollY;\n document.body.style.position = 'fixed';\n document.body.style.top = `-${scrollY}px`;\n document.body.style.width = '100%';\n sheetRef.current\n ?.querySelector<HTMLElement>('[role=\"spinbutton\"]')\n ?.focus();\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n\n setOpen(false);\n triggerRef.current?.focus();\n };\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.body.style.position = position;\n document.body.style.top = top;\n document.body.style.width = width;\n window.scrollTo(0, scrollY);\n };\n }, [open]);\n\n const openSheet = () => {\n if (disabled) return;\n\n setDraft(clampParts(partsOf(value ?? maxBirth), maxBirth));\n setOpen(true);\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const updateDraft = (patch: Partial<DateParts>) => {\n setDraft(prev => clampParts({ ...prev, ...patch }, maxBirth));\n };\n\n const apply = () => {\n onChange?.(toDate(draft));\n close();\n };\n\n // Текст не вводится: поле только выбирает дату через лист.\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Tab') return;\n\n event.preventDefault();\n if (\n event.key === 'Enter' ||\n event.key === ' ' ||\n event.key === 'ArrowDown'\n ) {\n openSheet();\n }\n };\n\n return (\n <div className={cn('date-picker', className)}>\n <Input\n ref={triggerRef}\n className=\"date-picker__trigger\"\n value={value ? formatDate(value) : ''}\n onChange={() => {}}\n onClick={openSheet}\n onKeyDown={handleTriggerKeyDown}\n onPaste={event => event.preventDefault()}\n inputMode=\"none\"\n autoComplete=\"off\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n placeholder={placeholder}\n disabled={disabled}\n error={error}\n />\n <IconCalendar className=\"date-picker__icon\" aria-hidden />\n {name ? (\n <input\n type=\"hidden\"\n name={name}\n value={value ? toIsoDate(value) : ''}\n />\n ) : null}\n\n {open\n ? createPortal(\n <div className=\"date-picker__layer\">\n <button\n type=\"button\"\n className=\"date-picker__mask\"\n aria-label=\"Закрыть\"\n onClick={close}\n />\n <dialog\n ref={sheetRef}\n open\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className=\"date-picker__sheet\"\n >\n <h2 id={titleId} className=\"date-picker__title\">\n {title}\n </h2>\n <div className=\"date-picker__columns\">\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">День</span>\n <WheelColumn\n label=\"День\"\n items={days.map(day => ({\n value: day,\n label: String(day),\n }))}\n value={draft.day}\n onChange={day => updateDraft({ day })}\n />\n </div>\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">Месяц</span>\n <WheelColumn\n label=\"Месяц\"\n items={MONTH_NAMES.map((month, index) => ({\n value: index + 1,\n label: month,\n }))}\n value={draft.month}\n onChange={month => updateDraft({ month })}\n />\n </div>\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">Год</span>\n <WheelColumn\n label=\"Год\"\n items={years.map(year => ({\n value: year,\n label: String(year),\n }))}\n value={draft.year}\n onChange={year => updateDraft({ year })}\n />\n </div>\n </div>\n <div className=\"date-picker__actions\">\n <Button onClick={apply}>\n <IconCorrect />\n Применить\n </Button>\n <Button variant=\"link\" onClick={close}>\n Отмена\n </Button>\n </div>\n </dialog>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n};\n"],"mappings":"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"}
|
package/dist/components/{birthday-picker/birthday-picker.d.ts → date-picker/date-picker.d.cts}
RENAMED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface DatePickerProps {
|
|
2
2
|
value?: Date | null;
|
|
3
3
|
onChange?: (date: Date) => void;
|
|
4
4
|
/** Самый ранний год в списке. */
|
|
5
5
|
minYear?: number;
|
|
6
6
|
/** Самый поздний год в списке. По умолчанию — текущий. */
|
|
7
7
|
maxYear?: number;
|
|
8
|
-
/** Минимальный возраст: позже
|
|
8
|
+
/** Минимальный возраст: позже сегодня минус столько лет выбрать нельзя. 0 — без ограничения. */
|
|
9
9
|
minAge?: number;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
/** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */
|
|
12
12
|
name?: string;
|
|
13
|
+
/** Текст в поле, пока дата не выбрана. */
|
|
13
14
|
placeholder?: string;
|
|
15
|
+
/** Заголовок окна выбора. */
|
|
16
|
+
title?: string;
|
|
14
17
|
error?: boolean;
|
|
15
18
|
className?: string;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
21
|
+
* Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.
|
|
19
22
|
* Поле только для чтения с клавиатуры, поэтому `required` из `Field` и
|
|
20
23
|
* нативная проверка формы работают, а телефонная клавиатура не открывается.
|
|
21
24
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
//# sourceMappingURL=
|
|
25
|
+
export declare const DatePicker: ({ value, onChange, minYear, maxYear, minAge, disabled, name, placeholder, title, error, className }: DatePickerProps) => import("react").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=date-picker.d.cts.map
|
package/dist/components/{birthday-picker/birthday-picker.d.cts → date-picker/date-picker.d.ts}
RENAMED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface DatePickerProps {
|
|
2
2
|
value?: Date | null;
|
|
3
3
|
onChange?: (date: Date) => void;
|
|
4
4
|
/** Самый ранний год в списке. */
|
|
5
5
|
minYear?: number;
|
|
6
6
|
/** Самый поздний год в списке. По умолчанию — текущий. */
|
|
7
7
|
maxYear?: number;
|
|
8
|
-
/** Минимальный возраст: позже
|
|
8
|
+
/** Минимальный возраст: позже сегодня минус столько лет выбрать нельзя. 0 — без ограничения. */
|
|
9
9
|
minAge?: number;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
/** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */
|
|
12
12
|
name?: string;
|
|
13
|
+
/** Текст в поле, пока дата не выбрана. */
|
|
13
14
|
placeholder?: string;
|
|
15
|
+
/** Заголовок окна выбора. */
|
|
16
|
+
title?: string;
|
|
14
17
|
error?: boolean;
|
|
15
18
|
className?: string;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
21
|
+
* Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.
|
|
19
22
|
* Поле только для чтения с клавиатуры, поэтому `required` из `Field` и
|
|
20
23
|
* нативная проверка формы работают, а телефонная клавиатура не открывается.
|
|
21
24
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
//# sourceMappingURL=
|
|
25
|
+
export declare const DatePicker: ({ value, onChange, minYear, maxYear, minAge, disabled, name, placeholder, title, error, className }: DatePickerProps) => import("react").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=date-picker.d.ts.map
|
|
@@ -8,8 +8,8 @@ import { useEffect, useId, useRef, useState } from "react";
|
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import { createPortal } from "react-dom";
|
|
10
10
|
import "../input/input.scss";
|
|
11
|
-
import "./
|
|
12
|
-
// src/components/
|
|
11
|
+
import "./date-picker.scss";
|
|
12
|
+
// src/components/date-picker/date-picker.tsx
|
|
13
13
|
const MONTH_NAMES = [
|
|
14
14
|
"январь",
|
|
15
15
|
"февраль",
|
|
@@ -47,9 +47,9 @@ const partsOf = (date) => ({
|
|
|
47
47
|
const toDate = ({ day, month, year }) => new Date(year, month - 1, day);
|
|
48
48
|
const pad = (value) => String(value).padStart(2, "0");
|
|
49
49
|
const toIsoDate = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
50
|
-
const
|
|
50
|
+
const formatDate = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
|
|
51
51
|
/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */
|
|
52
|
-
const
|
|
52
|
+
const maxAllowedDate = (minAge, today) => {
|
|
53
53
|
const year = today.getFullYear() - minAge;
|
|
54
54
|
const month = today.getMonth();
|
|
55
55
|
const day = Math.min(today.getDate(), daysInMonth(year, month + 1));
|
|
@@ -65,14 +65,14 @@ const clampParts = (parts, max) => {
|
|
|
65
65
|
};
|
|
66
66
|
const range = (from, to) => from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.
|
|
69
69
|
* Поле только для чтения с клавиатуры, поэтому `required` из `Field` и
|
|
70
70
|
* нативная проверка формы работают, а телефонная клавиатура не открывается.
|
|
71
71
|
*/
|
|
72
|
-
const
|
|
72
|
+
const DatePicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 0, disabled = false, name, placeholder = "Выберите дату", title = "Выберите дату", error, className }) => {
|
|
73
73
|
const [today] = useState(() => /* @__PURE__ */ new Date());
|
|
74
74
|
const [open, setOpen] = useState(false);
|
|
75
|
-
const maxBirth =
|
|
75
|
+
const maxBirth = maxAllowedDate(minAge, today);
|
|
76
76
|
const lastYear = Math.min(maxYear ?? today.getFullYear(), maxBirth.getFullYear());
|
|
77
77
|
const [draft, setDraft] = useState(() => clampParts(partsOf(value ?? maxBirth), maxBirth));
|
|
78
78
|
const triggerRef = useRef(null);
|
|
@@ -127,12 +127,12 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
127
127
|
if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") openSheet();
|
|
128
128
|
};
|
|
129
129
|
return /* @__PURE__ */ jsxs("div", {
|
|
130
|
-
className: cn("
|
|
130
|
+
className: cn("date-picker", className),
|
|
131
131
|
children: [
|
|
132
132
|
/* @__PURE__ */ jsx(Input, {
|
|
133
133
|
ref: triggerRef,
|
|
134
|
-
className: "
|
|
135
|
-
value: value ?
|
|
134
|
+
className: "date-picker__trigger",
|
|
135
|
+
value: value ? formatDate(value) : "",
|
|
136
136
|
onChange: () => {},
|
|
137
137
|
onClick: openSheet,
|
|
138
138
|
onKeyDown: handleTriggerKeyDown,
|
|
@@ -141,12 +141,12 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
141
141
|
autoComplete: "off",
|
|
142
142
|
"aria-haspopup": "dialog",
|
|
143
143
|
"aria-expanded": open,
|
|
144
|
-
placeholder
|
|
144
|
+
placeholder,
|
|
145
145
|
disabled,
|
|
146
146
|
error
|
|
147
147
|
}),
|
|
148
148
|
/* @__PURE__ */ jsx(IconCalendar, {
|
|
149
|
-
className: "
|
|
149
|
+
className: "date-picker__icon",
|
|
150
150
|
"aria-hidden": true
|
|
151
151
|
}),
|
|
152
152
|
name ? /* @__PURE__ */ jsx("input", {
|
|
@@ -155,10 +155,10 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
155
155
|
value: value ? toIsoDate(value) : ""
|
|
156
156
|
}) : null,
|
|
157
157
|
open ? createPortal(/* @__PURE__ */ jsxs("div", {
|
|
158
|
-
className: "
|
|
158
|
+
className: "date-picker__layer",
|
|
159
159
|
children: [/* @__PURE__ */ jsx("button", {
|
|
160
160
|
type: "button",
|
|
161
|
-
className: "
|
|
161
|
+
className: "date-picker__mask",
|
|
162
162
|
"aria-label": "Закрыть",
|
|
163
163
|
onClick: close
|
|
164
164
|
}), /* @__PURE__ */ jsxs("dialog", {
|
|
@@ -166,20 +166,20 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
166
166
|
open: true,
|
|
167
167
|
"aria-modal": "true",
|
|
168
168
|
"aria-labelledby": titleId,
|
|
169
|
-
className: "
|
|
169
|
+
className: "date-picker__sheet",
|
|
170
170
|
children: [
|
|
171
171
|
/* @__PURE__ */ jsx("h2", {
|
|
172
172
|
id: titleId,
|
|
173
|
-
className: "
|
|
174
|
-
children:
|
|
173
|
+
className: "date-picker__title",
|
|
174
|
+
children: title
|
|
175
175
|
}),
|
|
176
176
|
/* @__PURE__ */ jsxs("div", {
|
|
177
|
-
className: "
|
|
177
|
+
className: "date-picker__columns",
|
|
178
178
|
children: [
|
|
179
179
|
/* @__PURE__ */ jsxs("div", {
|
|
180
|
-
className: "
|
|
180
|
+
className: "date-picker__column",
|
|
181
181
|
children: [/* @__PURE__ */ jsx("span", {
|
|
182
|
-
className: "
|
|
182
|
+
className: "date-picker__caption",
|
|
183
183
|
children: "День"
|
|
184
184
|
}), /* @__PURE__ */ jsx(WheelColumn, {
|
|
185
185
|
label: "День",
|
|
@@ -192,9 +192,9 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
192
192
|
})]
|
|
193
193
|
}),
|
|
194
194
|
/* @__PURE__ */ jsxs("div", {
|
|
195
|
-
className: "
|
|
195
|
+
className: "date-picker__column",
|
|
196
196
|
children: [/* @__PURE__ */ jsx("span", {
|
|
197
|
-
className: "
|
|
197
|
+
className: "date-picker__caption",
|
|
198
198
|
children: "Месяц"
|
|
199
199
|
}), /* @__PURE__ */ jsx(WheelColumn, {
|
|
200
200
|
label: "Месяц",
|
|
@@ -207,9 +207,9 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
207
207
|
})]
|
|
208
208
|
}),
|
|
209
209
|
/* @__PURE__ */ jsxs("div", {
|
|
210
|
-
className: "
|
|
210
|
+
className: "date-picker__column",
|
|
211
211
|
children: [/* @__PURE__ */ jsx("span", {
|
|
212
|
-
className: "
|
|
212
|
+
className: "date-picker__caption",
|
|
213
213
|
children: "Год"
|
|
214
214
|
}), /* @__PURE__ */ jsx(WheelColumn, {
|
|
215
215
|
label: "Год",
|
|
@@ -224,7 +224,7 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
224
224
|
]
|
|
225
225
|
}),
|
|
226
226
|
/* @__PURE__ */ jsxs("div", {
|
|
227
|
-
className: "
|
|
227
|
+
className: "date-picker__actions",
|
|
228
228
|
children: [/* @__PURE__ */ jsxs(Button, {
|
|
229
229
|
onClick: apply,
|
|
230
230
|
children: [/* @__PURE__ */ jsx(IconCorrect, {}), "Применить"]
|
|
@@ -240,6 +240,6 @@ const BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18,
|
|
|
240
240
|
]
|
|
241
241
|
});
|
|
242
242
|
};
|
|
243
|
-
export {
|
|
243
|
+
export { DatePicker };
|
|
244
244
|
|
|
245
|
-
//# sourceMappingURL=
|
|
245
|
+
//# sourceMappingURL=date-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-picker.js","names":[],"sources":["../../../src/components/date-picker/date-picker.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Button } from '@/components/button';\nimport { Input } from '@/components/input';\nimport '../input/input.scss';\nimport { IconCalendar } from '@/icons/icon-calendar';\nimport { IconCorrect } from '@/icons/icon-correct';\nimport { cn } from '@/lib/cn';\nimport { WheelColumn } from './wheel-column';\nimport './date-picker.scss';\n\nconst MONTH_NAMES = [\n 'январь',\n 'февраль',\n 'март',\n 'апрель',\n 'май',\n 'июнь',\n 'июль',\n 'август',\n 'сентябрь',\n 'октябрь',\n 'ноябрь',\n 'декабрь',\n];\n\n// В дате пишется родительный падеж: «15 марта 1990».\nconst MONTH_NAMES_GENITIVE = [\n 'января',\n 'февраля',\n 'марта',\n 'апреля',\n 'мая',\n 'июня',\n 'июля',\n 'августа',\n 'сентября',\n 'октября',\n 'ноября',\n 'декабря',\n];\n\n/** Месяц здесь 1–12, как в календаре, а не индекс массива. */\ninterface DateParts {\n day: number;\n month: number;\n year: number;\n}\n\nconst daysInMonth = (year: number, month: number) =>\n new Date(year, month, 0).getDate();\n\nconst partsOf = (date: Date): DateParts => ({\n day: date.getDate(),\n month: date.getMonth() + 1,\n year: date.getFullYear(),\n});\n\nconst toDate = ({ day, month, year }: DateParts) =>\n new Date(year, month - 1, day);\n\nconst pad = (value: number) => String(value).padStart(2, '0');\n\nconst toIsoDate = (date: Date) =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\nconst formatDate = (date: Date) =>\n `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;\n\n/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */\nconst maxAllowedDate = (minAge: number, today: Date) => {\n const year = today.getFullYear() - minAge;\n const month = today.getMonth();\n const day = Math.min(today.getDate(), daysInMonth(year, month + 1));\n return new Date(year, month, day);\n};\n\n/** Подрезает день под длину месяца и не даёт выбрать дату позже максимальной. */\nconst clampParts = (parts: DateParts, max: Date): DateParts => {\n const next = {\n ...parts,\n day: Math.min(parts.day, daysInMonth(parts.year, parts.month)),\n };\n\n return toDate(next) > max ? partsOf(max) : next;\n};\n\nconst range = (from: number, to: number) =>\n from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);\n\nexport interface DatePickerProps {\n value?: Date | null;\n onChange?: (date: Date) => void;\n /** Самый ранний год в списке. */\n minYear?: number;\n /** Самый поздний год в списке. По умолчанию — текущий. */\n maxYear?: number;\n /** Минимальный возраст: позже сегодня минус столько лет выбрать нельзя. 0 — без ограничения. */\n minAge?: number;\n disabled?: boolean;\n /** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */\n name?: string;\n /** Текст в поле, пока дата не выбрана. */\n placeholder?: string;\n /** Заголовок окна выбора. */\n title?: string;\n error?: boolean;\n className?: string;\n}\n\n/**\n * Выбор даты: поле открывает нижний лист с выбором дня, месяца и года.\n * Поле только для чтения с клавиатуры, поэтому `required` из `Field` и\n * нативная проверка формы работают, а телефонная клавиатура не открывается.\n */\nexport const DatePicker = ({\n value,\n onChange,\n minYear = 1930,\n maxYear,\n minAge = 0,\n disabled = false,\n name,\n placeholder = 'Выберите дату',\n title = 'Выберите дату',\n error,\n className,\n}: DatePickerProps) => {\n const [today] = useState(() => new Date());\n const [open, setOpen] = useState(false);\n const maxBirth = maxAllowedDate(minAge, today);\n const lastYear = Math.min(\n maxYear ?? today.getFullYear(),\n maxBirth.getFullYear(),\n );\n const [draft, setDraft] = useState<DateParts>(() =>\n clampParts(partsOf(value ?? maxBirth), maxBirth),\n );\n const triggerRef = useRef<HTMLInputElement>(null);\n const sheetRef = useRef<HTMLDialogElement>(null);\n const titleId = useId();\n\n const years = range(Math.min(minYear, draft.year), lastYear);\n const days = range(1, daysInMonth(draft.year, draft.month));\n\n // iOS: без position: fixed фон прокручивается под открытым листом.\n useEffect(() => {\n if (!open) return;\n\n const { position, top, width } = document.body.style;\n const scrollY = window.scrollY;\n document.body.style.position = 'fixed';\n document.body.style.top = `-${scrollY}px`;\n document.body.style.width = '100%';\n sheetRef.current\n ?.querySelector<HTMLElement>('[role=\"spinbutton\"]')\n ?.focus();\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n\n setOpen(false);\n triggerRef.current?.focus();\n };\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.body.style.position = position;\n document.body.style.top = top;\n document.body.style.width = width;\n window.scrollTo(0, scrollY);\n };\n }, [open]);\n\n const openSheet = () => {\n if (disabled) return;\n\n setDraft(clampParts(partsOf(value ?? maxBirth), maxBirth));\n setOpen(true);\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const updateDraft = (patch: Partial<DateParts>) => {\n setDraft(prev => clampParts({ ...prev, ...patch }, maxBirth));\n };\n\n const apply = () => {\n onChange?.(toDate(draft));\n close();\n };\n\n // Текст не вводится: поле только выбирает дату через лист.\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Tab') return;\n\n event.preventDefault();\n if (\n event.key === 'Enter' ||\n event.key === ' ' ||\n event.key === 'ArrowDown'\n ) {\n openSheet();\n }\n };\n\n return (\n <div className={cn('date-picker', className)}>\n <Input\n ref={triggerRef}\n className=\"date-picker__trigger\"\n value={value ? formatDate(value) : ''}\n onChange={() => {}}\n onClick={openSheet}\n onKeyDown={handleTriggerKeyDown}\n onPaste={event => event.preventDefault()}\n inputMode=\"none\"\n autoComplete=\"off\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n placeholder={placeholder}\n disabled={disabled}\n error={error}\n />\n <IconCalendar className=\"date-picker__icon\" aria-hidden />\n {name ? (\n <input\n type=\"hidden\"\n name={name}\n value={value ? toIsoDate(value) : ''}\n />\n ) : null}\n\n {open\n ? createPortal(\n <div className=\"date-picker__layer\">\n <button\n type=\"button\"\n className=\"date-picker__mask\"\n aria-label=\"Закрыть\"\n onClick={close}\n />\n <dialog\n ref={sheetRef}\n open\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className=\"date-picker__sheet\"\n >\n <h2 id={titleId} className=\"date-picker__title\">\n {title}\n </h2>\n <div className=\"date-picker__columns\">\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">День</span>\n <WheelColumn\n label=\"День\"\n items={days.map(day => ({\n value: day,\n label: String(day),\n }))}\n value={draft.day}\n onChange={day => updateDraft({ day })}\n />\n </div>\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">Месяц</span>\n <WheelColumn\n label=\"Месяц\"\n items={MONTH_NAMES.map((month, index) => ({\n value: index + 1,\n label: month,\n }))}\n value={draft.month}\n onChange={month => updateDraft({ month })}\n />\n </div>\n <div className=\"date-picker__column\">\n <span className=\"date-picker__caption\">Год</span>\n <WheelColumn\n label=\"Год\"\n items={years.map(year => ({\n value: year,\n label: String(year),\n }))}\n value={draft.year}\n onChange={year => updateDraft({ year })}\n />\n </div>\n </div>\n <div className=\"date-picker__actions\">\n <Button onClick={apply}>\n <IconCorrect />\n Применить\n </Button>\n <Button variant=\"link\" onClick={close}>\n Отмена\n </Button>\n </div>\n </dialog>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n};\n"],"mappings":"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"}
|
package/dist/components/{birthday-picker/birthday-picker.scss → date-picker/date-picker.scss}
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
.
|
|
1
|
+
.date-picker {
|
|
2
2
|
position: relative;
|
|
3
3
|
width: 100%;
|
|
4
4
|
|
|
5
|
-
.
|
|
5
|
+
.date-picker__trigger {
|
|
6
6
|
cursor: pointer;
|
|
7
7
|
padding-right: 45px;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.
|
|
11
|
+
.date-picker__icon {
|
|
12
12
|
position: absolute;
|
|
13
13
|
top: 50%;
|
|
14
14
|
right: var(--space-3);
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
pointer-events: none;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
.
|
|
19
|
+
.date-picker__layer {
|
|
20
20
|
position: fixed;
|
|
21
21
|
inset: 0;
|
|
22
22
|
z-index: 1000;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.
|
|
25
|
+
.date-picker__mask {
|
|
26
26
|
position: absolute;
|
|
27
27
|
inset: 0;
|
|
28
28
|
padding: 0;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
cursor: pointer;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
.
|
|
34
|
+
.date-picker__sheet {
|
|
35
35
|
box-sizing: border-box;
|
|
36
36
|
position: absolute;
|
|
37
37
|
right: 0;
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
box-shadow: var(--shadow-xl);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.
|
|
52
|
+
.date-picker__title {
|
|
53
53
|
margin: 0 0 var(--space-3);
|
|
54
54
|
font-size: var(--text-sm);
|
|
55
55
|
font-weight: 700;
|
|
@@ -57,26 +57,26 @@
|
|
|
57
57
|
text-transform: uppercase;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
.
|
|
60
|
+
.date-picker__columns {
|
|
61
61
|
display: grid;
|
|
62
62
|
grid-template-columns: 1fr 1.5fr 1fr;
|
|
63
63
|
gap: var(--space-2);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
.
|
|
66
|
+
.date-picker__column {
|
|
67
67
|
display: grid;
|
|
68
68
|
gap: var(--space-1);
|
|
69
69
|
min-width: 0;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.
|
|
72
|
+
.date-picker__caption {
|
|
73
73
|
font-size: var(--text-xs);
|
|
74
74
|
color: var(--color-muted-foreground);
|
|
75
75
|
text-align: center;
|
|
76
76
|
text-transform: uppercase;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
.
|
|
79
|
+
.date-picker__actions {
|
|
80
80
|
display: flex;
|
|
81
81
|
justify-content: space-between;
|
|
82
82
|
align-items: center;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wheel-column.cjs","names":["useRef","cn","when"],"sources":["../../../src/components/date-picker/wheel-column.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type CSSProperties,\n type KeyboardEvent,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './wheel-column.scss';\n\nexport const WHEEL_ITEM_HEIGHT = 40;\nconst VISIBLE_ROWS = 5;\n/** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */\nconst SETTLE_DELAY = 120;\n\nexport interface WheelItem {\n value: number;\n label: string;\n}\n\nexport interface WheelColumnProps {\n /** Подпись для вспомогательных технологий. */\n label: string;\n items: WheelItem[];\n value: number;\n onChange: (value: number) => void;\n className?: string;\n}\n\nconst clamp = (index: number, max: number) => Math.min(Math.max(index, 0), max);\n\n/**\n * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.\n * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.\n */\nexport const WheelColumn = ({\n label,\n items,\n value,\n onChange,\n className,\n}: WheelColumnProps) => {\n const listRef = useRef<HTMLDivElement>(null);\n const settleTimer = useRef<number | undefined>(undefined);\n const frame = useRef<number | undefined>(undefined);\n const scrolling = useRef(false);\n // Обработчики из таймера видят актуальные значения, а не замыкание на момент скролла.\n const latest = useRef({ items, value, onChange });\n useEffect(() => {\n latest.current = { items, value, onChange };\n });\n\n const index = Math.max(\n 0,\n items.findIndex(item => item.value === value),\n );\n const selected = items[index];\n\n // Значение пришло снаружи или список сократился: докручиваем колесо.\n // Во время жеста пользователя не трогаем, иначе колесо дёргается под пальцем.\n useEffect(() => {\n const node = listRef.current;\n if (!node || scrolling.current) return;\n\n const target = index * WHEEL_ITEM_HEIGHT;\n if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;\n }, [index, items.length]);\n\n useEffect(\n () => () => {\n window.clearTimeout(settleTimer.current);\n if (frame.current !== undefined)\n window.cancelAnimationFrame(frame.current);\n },\n [],\n );\n\n // Выбор идёт за прокруткой, а не после остановки: тап по «Применить» во время\n // инерции не должен отстать от того, что видно на колесе.\n const reportPosition = () => {\n frame.current = undefined;\n const node = listRef.current;\n const { items: list, value: current, onChange: emit } = latest.current;\n if (!node || list.length === 0) return;\n\n const item =\n list[\n clamp(Math.round(node.scrollTop / WHEEL_ITEM_HEIGHT), list.length - 1)\n ];\n if (item && item.value !== current) emit(item.value);\n };\n\n const handleScroll = () => {\n scrolling.current = true;\n window.clearTimeout(settleTimer.current);\n settleTimer.current = window.setTimeout(() => {\n scrolling.current = false;\n }, SETTLE_DELAY);\n\n if (frame.current === undefined) {\n frame.current = window.requestAnimationFrame(reportPosition);\n }\n };\n\n const select = (next: number) => {\n const item = items[next];\n if (!item || next === index) return;\n onChange(item.value);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const steps: Record<string, number> = {\n ArrowUp: index - 1,\n ArrowDown: index + 1,\n PageUp: index - VISIBLE_ROWS,\n PageDown: index + VISIBLE_ROWS,\n Home: 0,\n End: items.length - 1,\n };\n\n const next = steps[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n select(clamp(next, items.length - 1));\n };\n\n const spacer = (\n <div\n className=\"wheel__spacer\"\n style={{ height: WHEEL_ITEM_HEIGHT * Math.floor(VISIBLE_ROWS / 2) }}\n aria-hidden\n />\n );\n\n return (\n <div\n className=\"wheel-frame\"\n style={\n { '--wheel-item-height': `${WHEEL_ITEM_HEIGHT}px` } as CSSProperties\n }\n >\n <div\n ref={listRef}\n // У колеса нет нативного тега; spinbutton — ближайшая роль с aria-valuenow.\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role\n role=\"spinbutton\"\n aria-label={label}\n aria-valuemin={items[0]?.value}\n aria-valuemax={items.at(-1)?.value}\n aria-valuenow={selected?.value}\n aria-valuetext={selected?.label}\n tabIndex={0}\n className={cn('wheel', className)}\n style={{ height: WHEEL_ITEM_HEIGHT * VISIBLE_ROWS }}\n onScroll={handleScroll}\n onKeyDown={handleKeyDown}\n >\n {spacer}\n {items.map((item, i) => (\n <button\n key={item.value}\n type=\"button\"\n tabIndex={-1}\n className={cn(\n 'wheel__item',\n when(i === index, 'wheel__item--active'),\n )}\n style={{ height: WHEEL_ITEM_HEIGHT }}\n onClick={() => select(i)}\n >\n {item.label}\n </button>\n ))}\n {spacer}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wheel-column.js","names":[],"sources":["../../../src/components/date-picker/wheel-column.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type CSSProperties,\n type KeyboardEvent,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './wheel-column.scss';\n\nexport const WHEEL_ITEM_HEIGHT = 40;\nconst VISIBLE_ROWS = 5;\n/** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */\nconst SETTLE_DELAY = 120;\n\nexport interface WheelItem {\n value: number;\n label: string;\n}\n\nexport interface WheelColumnProps {\n /** Подпись для вспомогательных технологий. */\n label: string;\n items: WheelItem[];\n value: number;\n onChange: (value: number) => void;\n className?: string;\n}\n\nconst clamp = (index: number, max: number) => Math.min(Math.max(index, 0), max);\n\n/**\n * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.\n * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.\n */\nexport const WheelColumn = ({\n label,\n items,\n value,\n onChange,\n className,\n}: WheelColumnProps) => {\n const listRef = useRef<HTMLDivElement>(null);\n const settleTimer = useRef<number | undefined>(undefined);\n const frame = useRef<number | undefined>(undefined);\n const scrolling = useRef(false);\n // Обработчики из таймера видят актуальные значения, а не замыкание на момент скролла.\n const latest = useRef({ items, value, onChange });\n useEffect(() => {\n latest.current = { items, value, onChange };\n });\n\n const index = Math.max(\n 0,\n items.findIndex(item => item.value === value),\n );\n const selected = items[index];\n\n // Значение пришло снаружи или список сократился: докручиваем колесо.\n // Во время жеста пользователя не трогаем, иначе колесо дёргается под пальцем.\n useEffect(() => {\n const node = listRef.current;\n if (!node || scrolling.current) return;\n\n const target = index * WHEEL_ITEM_HEIGHT;\n if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;\n }, [index, items.length]);\n\n useEffect(\n () => () => {\n window.clearTimeout(settleTimer.current);\n if (frame.current !== undefined)\n window.cancelAnimationFrame(frame.current);\n },\n [],\n );\n\n // Выбор идёт за прокруткой, а не после остановки: тап по «Применить» во время\n // инерции не должен отстать от того, что видно на колесе.\n const reportPosition = () => {\n frame.current = undefined;\n const node = listRef.current;\n const { items: list, value: current, onChange: emit } = latest.current;\n if (!node || list.length === 0) return;\n\n const item =\n list[\n clamp(Math.round(node.scrollTop / WHEEL_ITEM_HEIGHT), list.length - 1)\n ];\n if (item && item.value !== current) emit(item.value);\n };\n\n const handleScroll = () => {\n scrolling.current = true;\n window.clearTimeout(settleTimer.current);\n settleTimer.current = window.setTimeout(() => {\n scrolling.current = false;\n }, SETTLE_DELAY);\n\n if (frame.current === undefined) {\n frame.current = window.requestAnimationFrame(reportPosition);\n }\n };\n\n const select = (next: number) => {\n const item = items[next];\n if (!item || next === index) return;\n onChange(item.value);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const steps: Record<string, number> = {\n ArrowUp: index - 1,\n ArrowDown: index + 1,\n PageUp: index - VISIBLE_ROWS,\n PageDown: index + VISIBLE_ROWS,\n Home: 0,\n End: items.length - 1,\n };\n\n const next = steps[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n select(clamp(next, items.length - 1));\n };\n\n const spacer = (\n <div\n className=\"wheel__spacer\"\n style={{ height: WHEEL_ITEM_HEIGHT * Math.floor(VISIBLE_ROWS / 2) }}\n aria-hidden\n />\n );\n\n return (\n <div\n className=\"wheel-frame\"\n style={\n { '--wheel-item-height': `${WHEEL_ITEM_HEIGHT}px` } as CSSProperties\n }\n >\n <div\n ref={listRef}\n // У колеса нет нативного тега; spinbutton — ближайшая роль с aria-valuenow.\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role\n role=\"spinbutton\"\n aria-label={label}\n aria-valuemin={items[0]?.value}\n aria-valuemax={items.at(-1)?.value}\n aria-valuenow={selected?.value}\n aria-valuetext={selected?.label}\n tabIndex={0}\n className={cn('wheel', className)}\n style={{ height: WHEEL_ITEM_HEIGHT * VISIBLE_ROWS }}\n onScroll={handleScroll}\n onKeyDown={handleKeyDown}\n >\n {spacer}\n {items.map((item, i) => (\n <button\n key={item.value}\n type=\"button\"\n tabIndex={-1}\n className={cn(\n 'wheel__item',\n when(i === index, 'wheel__item--active'),\n )}\n style={{ height: WHEEL_ITEM_HEIGHT }}\n onClick={() => select(i)}\n >\n {item.label}\n </button>\n ))}\n {spacer}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
.field__label {
|
|
8
8
|
color: var(--color-foreground);
|
|
9
|
-
font-size: var(--text-
|
|
10
|
-
font-weight:
|
|
11
|
-
line-height: var(--text-
|
|
9
|
+
font-size: var(--text-xl);
|
|
10
|
+
font-weight: var(--font-weight-black);
|
|
11
|
+
line-height: var(--text-xl--line-height);
|
|
12
12
|
display: block;
|
|
13
13
|
margin-bottom: var(--space-3);
|
|
14
14
|
overflow-wrap: anywhere;
|
|
@@ -104,17 +104,18 @@
|
|
|
104
104
|
.input__clear {
|
|
105
105
|
position: absolute;
|
|
106
106
|
top: 0;
|
|
107
|
-
right:
|
|
107
|
+
right: 5px;
|
|
108
108
|
bottom: 0;
|
|
109
109
|
display: flex;
|
|
110
110
|
align-items: center;
|
|
111
111
|
justify-content: center;
|
|
112
|
-
width:
|
|
112
|
+
width: 30px;
|
|
113
113
|
margin: 0;
|
|
114
114
|
padding: 0;
|
|
115
115
|
border: 0;
|
|
116
116
|
background: none;
|
|
117
117
|
color: var(--color-muted-foreground);
|
|
118
|
+
cursor: pointer;
|
|
118
119
|
|
|
119
120
|
&:hover {
|
|
120
121
|
color: var(--color-foreground);
|
package/dist/index.cjs
CHANGED
|
@@ -4,16 +4,16 @@ const require_checkbox = require("./components/checkbox/checkbox.cjs");
|
|
|
4
4
|
const require_field = require("./components/field/field.cjs");
|
|
5
5
|
const require_form = require("./components/form/form.cjs");
|
|
6
6
|
const require_input = require("./components/input/input.cjs");
|
|
7
|
-
const
|
|
7
|
+
const require_date_picker = require("./components/date-picker/date-picker.cjs");
|
|
8
8
|
const require_hr = require("./components/hr/hr.cjs");
|
|
9
9
|
const require_radio = require("./components/radio/radio.cjs");
|
|
10
10
|
const require_phone_input = require("./components/phone-input/phone-input.cjs");
|
|
11
11
|
const require_spinner = require("./components/spinner/spinner.cjs");
|
|
12
12
|
const require_textarea = require("./components/textarea/textarea.cjs");
|
|
13
13
|
require("./styles/vars.scss");
|
|
14
|
-
exports.BirthdayPicker = require_birthday_picker.BirthdayPicker;
|
|
15
14
|
exports.Button = require_button.Button;
|
|
16
15
|
exports.CheckboxInput = require_checkbox.CheckboxInput;
|
|
16
|
+
exports.DatePicker = require_date_picker.DatePicker;
|
|
17
17
|
exports.Field = require_field.Field;
|
|
18
18
|
exports.Form = require_form.Form;
|
|
19
19
|
exports.FormActions = require_form.FormActions;
|
package/dist/index.d.cts
CHANGED
|
@@ -6,8 +6,8 @@ import { Field, FieldHintPlacement, FieldProps } from "./components/field/field.
|
|
|
6
6
|
import "./components/field/index.cjs";
|
|
7
7
|
import { Form, FormActions, FormActionsProps, FormConsent, FormConsentProps, FormProps, FormSubmit } from "./components/form/form.cjs";
|
|
8
8
|
import "./components/form/index.cjs";
|
|
9
|
-
import {
|
|
10
|
-
import "./components/
|
|
9
|
+
import { DatePicker, DatePickerProps } from "./components/date-picker/date-picker.cjs";
|
|
10
|
+
import "./components/date-picker/index.cjs";
|
|
11
11
|
import { HR, HRProps } from "./components/hr/hr.cjs";
|
|
12
12
|
import "./components/hr/index.cjs";
|
|
13
13
|
import { Input, InputProps, InputSize } from "./components/input/input.cjs";
|
|
@@ -20,4 +20,4 @@ import { Spinner, SpinnerPosition, SpinnerProps } from "./components/spinner/spi
|
|
|
20
20
|
import "./components/spinner/index.cjs";
|
|
21
21
|
import { TextArea, TextAreaProps, TextAreaSize } from "./components/textarea/textarea.cjs";
|
|
22
22
|
import "./components/textarea/index.cjs";
|
|
23
|
-
export {
|
|
23
|
+
export { Button, type ButtonAsChildProps, type ButtonComponentProps, type ButtonProps, type ButtonSize, type ButtonVariant, CheckboxInput, type CheckboxInputProps, DatePicker, type DatePickerProps, Field, type FieldHintPlacement, type FieldProps, Form, FormActions, type FormActionsProps, FormConsent, type FormConsentProps, type FormProps, FormSubmit, HR, type HRProps, Input, type InputProps, type InputSize, PHONE_MASK, PhoneInput, type PhoneInputProps, RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps, Spinner, type SpinnerPosition, type SpinnerProps, TextArea, type TextAreaProps, type TextAreaSize };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { Field, FieldHintPlacement, FieldProps } from "./components/field/field.
|
|
|
6
6
|
import "./components/field/index.js";
|
|
7
7
|
import { Form, FormActions, FormActionsProps, FormConsent, FormConsentProps, FormProps, FormSubmit } from "./components/form/form.js";
|
|
8
8
|
import "./components/form/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import "./components/
|
|
9
|
+
import { DatePicker, DatePickerProps } from "./components/date-picker/date-picker.js";
|
|
10
|
+
import "./components/date-picker/index.js";
|
|
11
11
|
import { HR, HRProps } from "./components/hr/hr.js";
|
|
12
12
|
import "./components/hr/index.js";
|
|
13
13
|
import { Input, InputProps, InputSize } from "./components/input/input.js";
|
|
@@ -20,4 +20,4 @@ import { Spinner, SpinnerPosition, SpinnerProps } from "./components/spinner/spi
|
|
|
20
20
|
import "./components/spinner/index.js";
|
|
21
21
|
import { TextArea, TextAreaProps, TextAreaSize } from "./components/textarea/textarea.js";
|
|
22
22
|
import "./components/textarea/index.js";
|
|
23
|
-
export {
|
|
23
|
+
export { Button, type ButtonAsChildProps, type ButtonComponentProps, type ButtonProps, type ButtonSize, type ButtonVariant, CheckboxInput, type CheckboxInputProps, DatePicker, type DatePickerProps, Field, type FieldHintPlacement, type FieldProps, Form, FormActions, type FormActionsProps, FormConsent, type FormConsentProps, type FormProps, FormSubmit, HR, type HRProps, Input, type InputProps, type InputSize, PHONE_MASK, PhoneInput, type PhoneInputProps, RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps, Spinner, type SpinnerPosition, type SpinnerProps, TextArea, type TextAreaProps, type TextAreaSize };
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import { CheckboxInput } from "./components/checkbox/checkbox.js";
|
|
|
3
3
|
import { Field } from "./components/field/field.js";
|
|
4
4
|
import { Form, FormActions, FormConsent, FormSubmit } from "./components/form/form.js";
|
|
5
5
|
import { Input } from "./components/input/input.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DatePicker } from "./components/date-picker/date-picker.js";
|
|
7
7
|
import { HR } from "./components/hr/hr.js";
|
|
8
8
|
import { RadioGroup, RadioInput } from "./components/radio/radio.js";
|
|
9
9
|
import { PHONE_MASK, PhoneInput } from "./components/phone-input/phone-input.js";
|
|
10
10
|
import { Spinner } from "./components/spinner/spinner.js";
|
|
11
11
|
import { TextArea } from "./components/textarea/textarea.js";
|
|
12
12
|
import "./styles/vars.scss";
|
|
13
|
-
export {
|
|
13
|
+
export { Button, CheckboxInput, DatePicker, Field, Form, FormActions, FormConsent, FormSubmit, HR, Input, PHONE_MASK, PhoneInput, RadioGroup, RadioInput, Spinner, TextArea };
|
package/dist/styles/global.scss
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// База приложения. Шрифт Inter подключает хост — файлы лежат в splittest-frontend/src/app/fonts.
|
|
2
2
|
// Токены (`--color-*`) подключает вход пакета, этот файл их не эмитит.
|
|
3
3
|
@use './mixins.scss' as mixins;
|
|
4
|
+
@use './typography.scss';
|
|
4
5
|
|
|
5
6
|
:root {
|
|
6
7
|
--window-max-height: 100vh;
|
|
@@ -167,35 +168,3 @@ table {
|
|
|
167
168
|
padding: 15px 0;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
-
h1,
|
|
172
|
-
h2,
|
|
173
|
-
h3 {
|
|
174
|
-
text-align: left;
|
|
175
|
-
font-weight: 900;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
h1 {
|
|
179
|
-
font-size: var(--text-2xl);
|
|
180
|
-
line-height: var(--text-2xl--line-height);
|
|
181
|
-
|
|
182
|
-
@include mixins.min-width(md) {
|
|
183
|
-
font-size: var(--text-3xl);
|
|
184
|
-
line-height: var(--text-3xl--line-height);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
h2 {
|
|
189
|
-
font-size: var(--text-2xl);
|
|
190
|
-
line-height: var(--text-2xl--line-height);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
h3 {
|
|
194
|
-
font-size: var(--text-lg);
|
|
195
|
-
line-height: var(--text-lg--line-height);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
h4 {
|
|
199
|
-
font-size: var(--text-sm);
|
|
200
|
-
line-height: var(--text-sm--line-height);
|
|
201
|
-
}
|
package/dist/styles/mixins.scss
CHANGED
|
@@ -49,3 +49,49 @@ $-length-units: 'px', 'em', 'rem';
|
|
|
49
49
|
@content;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/* Текст. Значения лежат в токенах vars.scss, миксины только подставляют их. */
|
|
54
|
+
@mixin text-small {
|
|
55
|
+
font-size: var(--text-xs);
|
|
56
|
+
line-height: var(--text-xs--line-height);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@mixin text-regular {
|
|
60
|
+
font-size: var(--text-sm);
|
|
61
|
+
line-height: var(--text-sm--line-height);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@mixin text-medium {
|
|
65
|
+
font-size: var(--text-base);
|
|
66
|
+
line-height: var(--text-base--line-height);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@mixin text-large {
|
|
70
|
+
font-size: var(--text-lg);
|
|
71
|
+
line-height: var(--text-lg--line-height);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Заголовки */
|
|
75
|
+
@mixin heading-h1 {
|
|
76
|
+
font-size: var(--text-4xl);
|
|
77
|
+
font-weight: var(--font-weight-black);
|
|
78
|
+
line-height: var(--text-4xl--line-height);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@mixin heading-h2 {
|
|
82
|
+
font-size: var(--text-3xl);
|
|
83
|
+
font-weight: var(--font-weight-black);
|
|
84
|
+
line-height: var(--text-3xl--line-height);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@mixin heading-h3 {
|
|
88
|
+
font-size: var(--text-2xl);
|
|
89
|
+
font-weight: var(--font-weight-black);
|
|
90
|
+
line-height: var(--text-2xl--line-height);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@mixin heading-h4 {
|
|
94
|
+
font-size: var(--text-xl);
|
|
95
|
+
font-weight: var(--font-weight-black);
|
|
96
|
+
line-height: var(--text-xl--line-height);
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
@use './mixins.scss' as mixins;
|
|
2
|
+
|
|
3
|
+
// Роли текста: класс задаёт вид, тег — семантику. Классы работают на любом элементе.
|
|
4
|
+
.text-xs {
|
|
5
|
+
@include mixins.text-small;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.text-sm {
|
|
9
|
+
@include mixins.text-regular;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.text-base {
|
|
13
|
+
@include mixins.text-medium;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.text-lg {
|
|
17
|
+
@include mixins.text-large;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.heading-1 {
|
|
21
|
+
@include mixins.heading-h1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.heading-2 {
|
|
25
|
+
@include mixins.heading-h2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.heading-3 {
|
|
29
|
+
@include mixins.heading-h3;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.heading-4 {
|
|
33
|
+
@include mixins.heading-h4;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Теги по умолчанию. h1–h3 меняются с 680px, как в вёрстке splittest.
|
|
37
|
+
h1,
|
|
38
|
+
h2,
|
|
39
|
+
h3,
|
|
40
|
+
h4 {
|
|
41
|
+
text-align: left;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
h1 {
|
|
45
|
+
@include mixins.heading-h3;
|
|
46
|
+
|
|
47
|
+
@include mixins.min-width(md) {
|
|
48
|
+
@include mixins.heading-h1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h2 {
|
|
53
|
+
@include mixins.heading-h3;
|
|
54
|
+
|
|
55
|
+
@include mixins.min-width(md) {
|
|
56
|
+
@include mixins.heading-h2;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
h3 {
|
|
61
|
+
@include mixins.heading-h4;
|
|
62
|
+
|
|
63
|
+
@include mixins.min-width(md) {
|
|
64
|
+
@include mixins.heading-h3;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
h4 {
|
|
69
|
+
@include mixins.heading-h4;
|
|
70
|
+
}
|
package/dist/styles/vars.scss
CHANGED
|
@@ -48,9 +48,14 @@
|
|
|
48
48
|
--radius-lg: 10px;
|
|
49
49
|
|
|
50
50
|
--font-sans: 'Inter', sans-serif;
|
|
51
|
+
--font-weight-black: 900;
|
|
52
|
+
--font-weight-bold: 700;
|
|
53
|
+
--font-weight-semibold: 600;
|
|
54
|
+
--font-weight-medium: 500;
|
|
55
|
+
--font-weight-normal: 400;
|
|
51
56
|
|
|
52
|
-
// Шкала
|
|
53
|
-
//
|
|
57
|
+
// Шкала текста в именах Tailwind: --text-<имя> и парная --text-<имя>--line-height.
|
|
58
|
+
// Размеры свои: 4xl — 45px, 3xl — 36px, 2xl — 27px, lg — 18px.
|
|
54
59
|
--text-xs: 11px;
|
|
55
60
|
--text-xs--line-height: 15px;
|
|
56
61
|
--text-sm: 14px;
|
|
@@ -58,11 +63,15 @@
|
|
|
58
63
|
--text-base: 16px;
|
|
59
64
|
--text-base--line-height: 20px;
|
|
60
65
|
--text-lg: 18px;
|
|
61
|
-
--text-lg--line-height:
|
|
62
|
-
--text-
|
|
66
|
+
--text-lg--line-height: 30px;
|
|
67
|
+
--text-xl: 18px;
|
|
68
|
+
--text-xl--line-height: 20px;
|
|
69
|
+
--text-2xl: 27px;
|
|
63
70
|
--text-2xl--line-height: 30px;
|
|
64
|
-
--text-3xl:
|
|
65
|
-
--text-3xl--line-height:
|
|
71
|
+
--text-3xl: 36px;
|
|
72
|
+
--text-3xl--line-height: 40px;
|
|
73
|
+
--text-4xl: 45px;
|
|
74
|
+
--text-4xl--line-height: 50px;
|
|
66
75
|
|
|
67
76
|
// Сетка 5px. Паддинги кнопки и инпута сняты с макета вне сетки и остаются литералами.
|
|
68
77
|
--space-1: 5px;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"birthday-picker.cjs","names":["useState","useRef","useId","cn","Input","IconCalendar","createPortal","WheelColumn","Button","IconCorrect"],"sources":["../../../src/components/birthday-picker/birthday-picker.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Button } from '@/components/button';\nimport { Input } from '@/components/input';\nimport '../input/input.scss';\nimport { IconCalendar } from '@/icons/icon-calendar';\nimport { IconCorrect } from '@/icons/icon-correct';\nimport { cn } from '@/lib/cn';\nimport { WheelColumn } from './wheel-column';\nimport './birthday-picker.scss';\n\nconst MONTH_NAMES = [\n 'январь',\n 'февраль',\n 'март',\n 'апрель',\n 'май',\n 'июнь',\n 'июль',\n 'август',\n 'сентябрь',\n 'октябрь',\n 'ноябрь',\n 'декабрь',\n];\n\n// В дате пишется родительный падеж: «15 марта 1990».\nconst MONTH_NAMES_GENITIVE = [\n 'января',\n 'февраля',\n 'марта',\n 'апреля',\n 'мая',\n 'июня',\n 'июля',\n 'августа',\n 'сентября',\n 'октября',\n 'ноября',\n 'декабря',\n];\n\n/** Месяц здесь 1–12, как в календаре, а не индекс массива. */\ninterface BirthdayParts {\n day: number;\n month: number;\n year: number;\n}\n\nconst daysInMonth = (year: number, month: number) =>\n new Date(year, month, 0).getDate();\n\nconst partsOf = (date: Date): BirthdayParts => ({\n day: date.getDate(),\n month: date.getMonth() + 1,\n year: date.getFullYear(),\n});\n\nconst toDate = ({ day, month, year }: BirthdayParts) =>\n new Date(year, month - 1, day);\n\nconst pad = (value: number) => String(value).padStart(2, '0');\n\nconst toIsoDate = (date: Date) =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\nconst formatBirthday = (date: Date) =>\n `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;\n\n/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */\nconst maxBirthDate = (minAge: number, today: Date) => {\n const year = today.getFullYear() - minAge;\n const month = today.getMonth();\n const day = Math.min(today.getDate(), daysInMonth(year, month + 1));\n return new Date(year, month, day);\n};\n\n/** Подрезает день под длину месяца и не даёт выбрать дату позже максимальной. */\nconst clampParts = (parts: BirthdayParts, max: Date): BirthdayParts => {\n const next = {\n ...parts,\n day: Math.min(parts.day, daysInMonth(parts.year, parts.month)),\n };\n\n return toDate(next) > max ? partsOf(max) : next;\n};\n\nconst range = (from: number, to: number) =>\n from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);\n\nexport interface BirthdayPickerProps {\n value?: Date | null;\n onChange?: (date: Date) => void;\n /** Самый ранний год в списке. */\n minYear?: number;\n /** Самый поздний год в списке. По умолчанию — текущий. */\n maxYear?: number;\n /** Минимальный возраст: позже этой даты выбрать нельзя. */\n minAge?: number;\n disabled?: boolean;\n /** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */\n name?: string;\n placeholder?: string;\n error?: boolean;\n className?: string;\n}\n\n/**\n * Дата рождения: поле открывает нижний лист с выбором дня, месяца и года.\n * Поле только для чтения с клавиатуры, поэтому `required` из `Field` и\n * нативная проверка формы работают, а телефонная клавиатура не открывается.\n */\nexport const BirthdayPicker = ({\n value,\n onChange,\n minYear = 1930,\n maxYear,\n minAge = 18,\n disabled = false,\n name,\n placeholder,\n error,\n className,\n}: BirthdayPickerProps) => {\n const [today] = useState(() => new Date());\n const [open, setOpen] = useState(false);\n const maxBirth = maxBirthDate(minAge, today);\n const lastYear = Math.min(\n maxYear ?? today.getFullYear(),\n maxBirth.getFullYear(),\n );\n const [draft, setDraft] = useState<BirthdayParts>(() =>\n clampParts(partsOf(value ?? maxBirth), maxBirth),\n );\n const triggerRef = useRef<HTMLInputElement>(null);\n const sheetRef = useRef<HTMLDialogElement>(null);\n const titleId = useId();\n\n const years = range(Math.min(minYear, draft.year), lastYear);\n const days = range(1, daysInMonth(draft.year, draft.month));\n\n // iOS: без position: fixed фон прокручивается под открытым листом.\n useEffect(() => {\n if (!open) return;\n\n const { position, top, width } = document.body.style;\n const scrollY = window.scrollY;\n document.body.style.position = 'fixed';\n document.body.style.top = `-${scrollY}px`;\n document.body.style.width = '100%';\n sheetRef.current\n ?.querySelector<HTMLElement>('[role=\"spinbutton\"]')\n ?.focus();\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n\n setOpen(false);\n triggerRef.current?.focus();\n };\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.body.style.position = position;\n document.body.style.top = top;\n document.body.style.width = width;\n window.scrollTo(0, scrollY);\n };\n }, [open]);\n\n const openSheet = () => {\n if (disabled) return;\n\n setDraft(clampParts(partsOf(value ?? maxBirth), maxBirth));\n setOpen(true);\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const updateDraft = (patch: Partial<BirthdayParts>) => {\n setDraft(prev => clampParts({ ...prev, ...patch }, maxBirth));\n };\n\n const apply = () => {\n onChange?.(toDate(draft));\n close();\n };\n\n // Текст не вводится: поле только выбирает дату через лист.\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Tab') return;\n\n event.preventDefault();\n if (\n event.key === 'Enter' ||\n event.key === ' ' ||\n event.key === 'ArrowDown'\n ) {\n openSheet();\n }\n };\n\n return (\n <div className={cn('birthday-picker', className)}>\n <Input\n ref={triggerRef}\n className=\"birthday-picker__trigger\"\n value={value ? formatBirthday(value) : ''}\n onChange={() => {}}\n onClick={openSheet}\n onKeyDown={handleTriggerKeyDown}\n onPaste={event => event.preventDefault()}\n inputMode=\"none\"\n autoComplete=\"off\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n placeholder={placeholder ?? `Возраст от ${minAge} лет`}\n disabled={disabled}\n error={error}\n />\n <IconCalendar className=\"birthday-picker__icon\" aria-hidden />\n {name ? (\n <input\n type=\"hidden\"\n name={name}\n value={value ? toIsoDate(value) : ''}\n />\n ) : null}\n\n {open\n ? createPortal(\n <div className=\"birthday-picker__layer\">\n <button\n type=\"button\"\n className=\"birthday-picker__mask\"\n aria-label=\"Закрыть\"\n onClick={close}\n />\n <dialog\n ref={sheetRef}\n open\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className=\"birthday-picker__sheet\"\n >\n <h2 id={titleId} className=\"birthday-picker__title\">\n Дата рождения\n </h2>\n <div className=\"birthday-picker__columns\">\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">День</span>\n <WheelColumn\n label=\"День\"\n items={days.map(day => ({\n value: day,\n label: String(day),\n }))}\n value={draft.day}\n onChange={day => updateDraft({ day })}\n />\n </div>\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">Месяц</span>\n <WheelColumn\n label=\"Месяц\"\n items={MONTH_NAMES.map((month, index) => ({\n value: index + 1,\n label: month,\n }))}\n value={draft.month}\n onChange={month => updateDraft({ month })}\n />\n </div>\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">Год</span>\n <WheelColumn\n label=\"Год\"\n items={years.map(year => ({\n value: year,\n label: String(year),\n }))}\n value={draft.year}\n onChange={year => updateDraft({ year })}\n />\n </div>\n </div>\n <div className=\"birthday-picker__actions\">\n <Button onClick={apply}>\n <IconCorrect />\n Применить\n </Button>\n <Button variant=\"link\" onClick={close}>\n Отмена\n </Button>\n </div>\n </dialog>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"birthday-picker.js","names":[],"sources":["../../../src/components/birthday-picker/birthday-picker.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Button } from '@/components/button';\nimport { Input } from '@/components/input';\nimport '../input/input.scss';\nimport { IconCalendar } from '@/icons/icon-calendar';\nimport { IconCorrect } from '@/icons/icon-correct';\nimport { cn } from '@/lib/cn';\nimport { WheelColumn } from './wheel-column';\nimport './birthday-picker.scss';\n\nconst MONTH_NAMES = [\n 'январь',\n 'февраль',\n 'март',\n 'апрель',\n 'май',\n 'июнь',\n 'июль',\n 'август',\n 'сентябрь',\n 'октябрь',\n 'ноябрь',\n 'декабрь',\n];\n\n// В дате пишется родительный падеж: «15 марта 1990».\nconst MONTH_NAMES_GENITIVE = [\n 'января',\n 'февраля',\n 'марта',\n 'апреля',\n 'мая',\n 'июня',\n 'июля',\n 'августа',\n 'сентября',\n 'октября',\n 'ноября',\n 'декабря',\n];\n\n/** Месяц здесь 1–12, как в календаре, а не индекс массива. */\ninterface BirthdayParts {\n day: number;\n month: number;\n year: number;\n}\n\nconst daysInMonth = (year: number, month: number) =>\n new Date(year, month, 0).getDate();\n\nconst partsOf = (date: Date): BirthdayParts => ({\n day: date.getDate(),\n month: date.getMonth() + 1,\n year: date.getFullYear(),\n});\n\nconst toDate = ({ day, month, year }: BirthdayParts) =>\n new Date(year, month - 1, day);\n\nconst pad = (value: number) => String(value).padStart(2, '0');\n\nconst toIsoDate = (date: Date) =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\nconst formatBirthday = (date: Date) =>\n `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;\n\n/** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */\nconst maxBirthDate = (minAge: number, today: Date) => {\n const year = today.getFullYear() - minAge;\n const month = today.getMonth();\n const day = Math.min(today.getDate(), daysInMonth(year, month + 1));\n return new Date(year, month, day);\n};\n\n/** Подрезает день под длину месяца и не даёт выбрать дату позже максимальной. */\nconst clampParts = (parts: BirthdayParts, max: Date): BirthdayParts => {\n const next = {\n ...parts,\n day: Math.min(parts.day, daysInMonth(parts.year, parts.month)),\n };\n\n return toDate(next) > max ? partsOf(max) : next;\n};\n\nconst range = (from: number, to: number) =>\n from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i);\n\nexport interface BirthdayPickerProps {\n value?: Date | null;\n onChange?: (date: Date) => void;\n /** Самый ранний год в списке. */\n minYear?: number;\n /** Самый поздний год в списке. По умолчанию — текущий. */\n maxYear?: number;\n /** Минимальный возраст: позже этой даты выбрать нельзя. */\n minAge?: number;\n disabled?: boolean;\n /** Имя для FormData: уходит дата в формате `YYYY-MM-DD`. */\n name?: string;\n placeholder?: string;\n error?: boolean;\n className?: string;\n}\n\n/**\n * Дата рождения: поле открывает нижний лист с выбором дня, месяца и года.\n * Поле только для чтения с клавиатуры, поэтому `required` из `Field` и\n * нативная проверка формы работают, а телефонная клавиатура не открывается.\n */\nexport const BirthdayPicker = ({\n value,\n onChange,\n minYear = 1930,\n maxYear,\n minAge = 18,\n disabled = false,\n name,\n placeholder,\n error,\n className,\n}: BirthdayPickerProps) => {\n const [today] = useState(() => new Date());\n const [open, setOpen] = useState(false);\n const maxBirth = maxBirthDate(minAge, today);\n const lastYear = Math.min(\n maxYear ?? today.getFullYear(),\n maxBirth.getFullYear(),\n );\n const [draft, setDraft] = useState<BirthdayParts>(() =>\n clampParts(partsOf(value ?? maxBirth), maxBirth),\n );\n const triggerRef = useRef<HTMLInputElement>(null);\n const sheetRef = useRef<HTMLDialogElement>(null);\n const titleId = useId();\n\n const years = range(Math.min(minYear, draft.year), lastYear);\n const days = range(1, daysInMonth(draft.year, draft.month));\n\n // iOS: без position: fixed фон прокручивается под открытым листом.\n useEffect(() => {\n if (!open) return;\n\n const { position, top, width } = document.body.style;\n const scrollY = window.scrollY;\n document.body.style.position = 'fixed';\n document.body.style.top = `-${scrollY}px`;\n document.body.style.width = '100%';\n sheetRef.current\n ?.querySelector<HTMLElement>('[role=\"spinbutton\"]')\n ?.focus();\n\n const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape') return;\n\n setOpen(false);\n triggerRef.current?.focus();\n };\n document.addEventListener('keydown', handleKeyDown);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.body.style.position = position;\n document.body.style.top = top;\n document.body.style.width = width;\n window.scrollTo(0, scrollY);\n };\n }, [open]);\n\n const openSheet = () => {\n if (disabled) return;\n\n setDraft(clampParts(partsOf(value ?? maxBirth), maxBirth));\n setOpen(true);\n };\n\n const close = () => {\n setOpen(false);\n triggerRef.current?.focus();\n };\n\n const updateDraft = (patch: Partial<BirthdayParts>) => {\n setDraft(prev => clampParts({ ...prev, ...patch }, maxBirth));\n };\n\n const apply = () => {\n onChange?.(toDate(draft));\n close();\n };\n\n // Текст не вводится: поле только выбирает дату через лист.\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Tab') return;\n\n event.preventDefault();\n if (\n event.key === 'Enter' ||\n event.key === ' ' ||\n event.key === 'ArrowDown'\n ) {\n openSheet();\n }\n };\n\n return (\n <div className={cn('birthday-picker', className)}>\n <Input\n ref={triggerRef}\n className=\"birthday-picker__trigger\"\n value={value ? formatBirthday(value) : ''}\n onChange={() => {}}\n onClick={openSheet}\n onKeyDown={handleTriggerKeyDown}\n onPaste={event => event.preventDefault()}\n inputMode=\"none\"\n autoComplete=\"off\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n placeholder={placeholder ?? `Возраст от ${minAge} лет`}\n disabled={disabled}\n error={error}\n />\n <IconCalendar className=\"birthday-picker__icon\" aria-hidden />\n {name ? (\n <input\n type=\"hidden\"\n name={name}\n value={value ? toIsoDate(value) : ''}\n />\n ) : null}\n\n {open\n ? createPortal(\n <div className=\"birthday-picker__layer\">\n <button\n type=\"button\"\n className=\"birthday-picker__mask\"\n aria-label=\"Закрыть\"\n onClick={close}\n />\n <dialog\n ref={sheetRef}\n open\n aria-modal=\"true\"\n aria-labelledby={titleId}\n className=\"birthday-picker__sheet\"\n >\n <h2 id={titleId} className=\"birthday-picker__title\">\n Дата рождения\n </h2>\n <div className=\"birthday-picker__columns\">\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">День</span>\n <WheelColumn\n label=\"День\"\n items={days.map(day => ({\n value: day,\n label: String(day),\n }))}\n value={draft.day}\n onChange={day => updateDraft({ day })}\n />\n </div>\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">Месяц</span>\n <WheelColumn\n label=\"Месяц\"\n items={MONTH_NAMES.map((month, index) => ({\n value: index + 1,\n label: month,\n }))}\n value={draft.month}\n onChange={month => updateDraft({ month })}\n />\n </div>\n <div className=\"birthday-picker__column\">\n <span className=\"birthday-picker__caption\">Год</span>\n <WheelColumn\n label=\"Год\"\n items={years.map(year => ({\n value: year,\n label: String(year),\n }))}\n value={draft.year}\n onChange={year => updateDraft({ year })}\n />\n </div>\n </div>\n <div className=\"birthday-picker__actions\">\n <Button onClick={apply}>\n <IconCorrect />\n Применить\n </Button>\n <Button variant=\"link\" onClick={close}>\n Отмена\n </Button>\n </div>\n </dialog>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wheel-column.cjs","names":["useRef","cn","when"],"sources":["../../../src/components/birthday-picker/wheel-column.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type CSSProperties,\n type KeyboardEvent,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './wheel-column.scss';\n\nexport const WHEEL_ITEM_HEIGHT = 40;\nconst VISIBLE_ROWS = 5;\n/** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */\nconst SETTLE_DELAY = 120;\n\nexport interface WheelItem {\n value: number;\n label: string;\n}\n\nexport interface WheelColumnProps {\n /** Подпись для вспомогательных технологий. */\n label: string;\n items: WheelItem[];\n value: number;\n onChange: (value: number) => void;\n className?: string;\n}\n\nconst clamp = (index: number, max: number) => Math.min(Math.max(index, 0), max);\n\n/**\n * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.\n * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.\n */\nexport const WheelColumn = ({\n label,\n items,\n value,\n onChange,\n className,\n}: WheelColumnProps) => {\n const listRef = useRef<HTMLDivElement>(null);\n const settleTimer = useRef<number | undefined>(undefined);\n const frame = useRef<number | undefined>(undefined);\n const scrolling = useRef(false);\n // Обработчики из таймера видят актуальные значения, а не замыкание на момент скролла.\n const latest = useRef({ items, value, onChange });\n useEffect(() => {\n latest.current = { items, value, onChange };\n });\n\n const index = Math.max(\n 0,\n items.findIndex(item => item.value === value),\n );\n const selected = items[index];\n\n // Значение пришло снаружи или список сократился: докручиваем колесо.\n // Во время жеста пользователя не трогаем, иначе колесо дёргается под пальцем.\n useEffect(() => {\n const node = listRef.current;\n if (!node || scrolling.current) return;\n\n const target = index * WHEEL_ITEM_HEIGHT;\n if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;\n }, [index, items.length]);\n\n useEffect(\n () => () => {\n window.clearTimeout(settleTimer.current);\n if (frame.current !== undefined)\n window.cancelAnimationFrame(frame.current);\n },\n [],\n );\n\n // Выбор идёт за прокруткой, а не после остановки: тап по «Применить» во время\n // инерции не должен отстать от того, что видно на колесе.\n const reportPosition = () => {\n frame.current = undefined;\n const node = listRef.current;\n const { items: list, value: current, onChange: emit } = latest.current;\n if (!node || list.length === 0) return;\n\n const item =\n list[\n clamp(Math.round(node.scrollTop / WHEEL_ITEM_HEIGHT), list.length - 1)\n ];\n if (item && item.value !== current) emit(item.value);\n };\n\n const handleScroll = () => {\n scrolling.current = true;\n window.clearTimeout(settleTimer.current);\n settleTimer.current = window.setTimeout(() => {\n scrolling.current = false;\n }, SETTLE_DELAY);\n\n if (frame.current === undefined) {\n frame.current = window.requestAnimationFrame(reportPosition);\n }\n };\n\n const select = (next: number) => {\n const item = items[next];\n if (!item || next === index) return;\n onChange(item.value);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const steps: Record<string, number> = {\n ArrowUp: index - 1,\n ArrowDown: index + 1,\n PageUp: index - VISIBLE_ROWS,\n PageDown: index + VISIBLE_ROWS,\n Home: 0,\n End: items.length - 1,\n };\n\n const next = steps[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n select(clamp(next, items.length - 1));\n };\n\n const spacer = (\n <div\n className=\"wheel__spacer\"\n style={{ height: WHEEL_ITEM_HEIGHT * Math.floor(VISIBLE_ROWS / 2) }}\n aria-hidden\n />\n );\n\n return (\n <div\n className=\"wheel-frame\"\n style={\n { '--wheel-item-height': `${WHEEL_ITEM_HEIGHT}px` } as CSSProperties\n }\n >\n <div\n ref={listRef}\n // У колеса нет нативного тега; spinbutton — ближайшая роль с aria-valuenow.\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role\n role=\"spinbutton\"\n aria-label={label}\n aria-valuemin={items[0]?.value}\n aria-valuemax={items.at(-1)?.value}\n aria-valuenow={selected?.value}\n aria-valuetext={selected?.label}\n tabIndex={0}\n className={cn('wheel', className)}\n style={{ height: WHEEL_ITEM_HEIGHT * VISIBLE_ROWS }}\n onScroll={handleScroll}\n onKeyDown={handleKeyDown}\n >\n {spacer}\n {items.map((item, i) => (\n <button\n key={item.value}\n type=\"button\"\n tabIndex={-1}\n className={cn(\n 'wheel__item',\n when(i === index, 'wheel__item--active'),\n )}\n style={{ height: WHEEL_ITEM_HEIGHT }}\n onClick={() => select(i)}\n >\n {item.label}\n </button>\n ))}\n {spacer}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"wheel-column.js","names":[],"sources":["../../../src/components/birthday-picker/wheel-column.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type CSSProperties,\n type KeyboardEvent,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './wheel-column.scss';\n\nexport const WHEEL_ITEM_HEIGHT = 40;\nconst VISIBLE_ROWS = 5;\n/** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */\nconst SETTLE_DELAY = 120;\n\nexport interface WheelItem {\n value: number;\n label: string;\n}\n\nexport interface WheelColumnProps {\n /** Подпись для вспомогательных технологий. */\n label: string;\n items: WheelItem[];\n value: number;\n onChange: (value: number) => void;\n className?: string;\n}\n\nconst clamp = (index: number, max: number) => Math.min(Math.max(index, 0), max);\n\n/**\n * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.\n * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.\n */\nexport const WheelColumn = ({\n label,\n items,\n value,\n onChange,\n className,\n}: WheelColumnProps) => {\n const listRef = useRef<HTMLDivElement>(null);\n const settleTimer = useRef<number | undefined>(undefined);\n const frame = useRef<number | undefined>(undefined);\n const scrolling = useRef(false);\n // Обработчики из таймера видят актуальные значения, а не замыкание на момент скролла.\n const latest = useRef({ items, value, onChange });\n useEffect(() => {\n latest.current = { items, value, onChange };\n });\n\n const index = Math.max(\n 0,\n items.findIndex(item => item.value === value),\n );\n const selected = items[index];\n\n // Значение пришло снаружи или список сократился: докручиваем колесо.\n // Во время жеста пользователя не трогаем, иначе колесо дёргается под пальцем.\n useEffect(() => {\n const node = listRef.current;\n if (!node || scrolling.current) return;\n\n const target = index * WHEEL_ITEM_HEIGHT;\n if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;\n }, [index, items.length]);\n\n useEffect(\n () => () => {\n window.clearTimeout(settleTimer.current);\n if (frame.current !== undefined)\n window.cancelAnimationFrame(frame.current);\n },\n [],\n );\n\n // Выбор идёт за прокруткой, а не после остановки: тап по «Применить» во время\n // инерции не должен отстать от того, что видно на колесе.\n const reportPosition = () => {\n frame.current = undefined;\n const node = listRef.current;\n const { items: list, value: current, onChange: emit } = latest.current;\n if (!node || list.length === 0) return;\n\n const item =\n list[\n clamp(Math.round(node.scrollTop / WHEEL_ITEM_HEIGHT), list.length - 1)\n ];\n if (item && item.value !== current) emit(item.value);\n };\n\n const handleScroll = () => {\n scrolling.current = true;\n window.clearTimeout(settleTimer.current);\n settleTimer.current = window.setTimeout(() => {\n scrolling.current = false;\n }, SETTLE_DELAY);\n\n if (frame.current === undefined) {\n frame.current = window.requestAnimationFrame(reportPosition);\n }\n };\n\n const select = (next: number) => {\n const item = items[next];\n if (!item || next === index) return;\n onChange(item.value);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const steps: Record<string, number> = {\n ArrowUp: index - 1,\n ArrowDown: index + 1,\n PageUp: index - VISIBLE_ROWS,\n PageDown: index + VISIBLE_ROWS,\n Home: 0,\n End: items.length - 1,\n };\n\n const next = steps[event.key];\n if (next === undefined) return;\n\n event.preventDefault();\n select(clamp(next, items.length - 1));\n };\n\n const spacer = (\n <div\n className=\"wheel__spacer\"\n style={{ height: WHEEL_ITEM_HEIGHT * Math.floor(VISIBLE_ROWS / 2) }}\n aria-hidden\n />\n );\n\n return (\n <div\n className=\"wheel-frame\"\n style={\n { '--wheel-item-height': `${WHEEL_ITEM_HEIGHT}px` } as CSSProperties\n }\n >\n <div\n ref={listRef}\n // У колеса нет нативного тега; spinbutton — ближайшая роль с aria-valuenow.\n // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role\n role=\"spinbutton\"\n aria-label={label}\n aria-valuemin={items[0]?.value}\n aria-valuemax={items.at(-1)?.value}\n aria-valuenow={selected?.value}\n aria-valuetext={selected?.label}\n tabIndex={0}\n className={cn('wheel', className)}\n style={{ height: WHEEL_ITEM_HEIGHT * VISIBLE_ROWS }}\n onScroll={handleScroll}\n onKeyDown={handleKeyDown}\n >\n {spacer}\n {items.map((item, i) => (\n <button\n key={item.value}\n type=\"button\"\n tabIndex={-1}\n className={cn(\n 'wheel__item',\n when(i === index, 'wheel__item--active'),\n )}\n style={{ height: WHEEL_ITEM_HEIGHT }}\n onClick={() => select(i)}\n >\n {item.label}\n </button>\n ))}\n {spacer}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|