@de-kit/st-ui 0.1.16 → 0.1.17

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.
Files changed (30) hide show
  1. package/dist/components/{birthday-picker/birthday-picker.cjs → date-picker/date-picker.cjs} +27 -27
  2. package/dist/components/date-picker/date-picker.cjs.map +1 -0
  3. package/dist/components/{birthday-picker/birthday-picker.d.ts → date-picker/date-picker.d.cts} +8 -5
  4. package/dist/components/{birthday-picker/birthday-picker.d.cts → date-picker/date-picker.d.ts} +8 -5
  5. package/dist/components/{birthday-picker/birthday-picker.js → date-picker/date-picker.js} +27 -27
  6. package/dist/components/date-picker/date-picker.js.map +1 -0
  7. package/dist/components/{birthday-picker/birthday-picker.scss → date-picker/date-picker.scss} +11 -11
  8. package/dist/components/date-picker/index.d.cts +2 -0
  9. package/dist/components/date-picker/index.d.ts +2 -0
  10. package/dist/components/date-picker/wheel-column.cjs.map +1 -0
  11. package/dist/components/date-picker/wheel-column.js.map +1 -0
  12. package/dist/components/field/field.scss +3 -3
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +2 -2
  17. package/dist/styles/global.scss +1 -32
  18. package/dist/styles/mixins.scss +46 -0
  19. package/dist/styles/typography.scss +70 -0
  20. package/dist/styles/vars.scss +11 -6
  21. package/package.json +1 -1
  22. package/dist/components/birthday-picker/birthday-picker.cjs.map +0 -1
  23. package/dist/components/birthday-picker/birthday-picker.js.map +0 -1
  24. package/dist/components/birthday-picker/index.d.cts +0 -2
  25. package/dist/components/birthday-picker/index.d.ts +0 -2
  26. package/dist/components/birthday-picker/wheel-column.cjs.map +0 -1
  27. package/dist/components/birthday-picker/wheel-column.js.map +0 -1
  28. /package/dist/components/{birthday-picker → date-picker}/wheel-column.cjs +0 -0
  29. /package/dist/components/{birthday-picker → date-picker}/wheel-column.js +0 -0
  30. /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("./birthday-picker.scss");
12
- // src/components/birthday-picker/birthday-picker.tsx
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 formatBirthday = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
50
+ const formatDate = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
51
51
  /** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */
52
- const maxBirthDate = (minAge, today) => {
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 BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18, disabled = false, name, placeholder, error, className }) => {
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 = maxBirthDate(minAge, today);
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("birthday-picker", className),
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: "birthday-picker__trigger",
135
- value: value ? formatBirthday(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: placeholder ?? `Возраст от ${minAge} лет`,
144
+ placeholder,
145
145
  disabled,
146
146
  error
147
147
  }),
148
148
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_calendar.IconCalendar, {
149
- className: "birthday-picker__icon",
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: "birthday-picker__layer",
158
+ className: "date-picker__layer",
159
159
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
160
160
  type: "button",
161
- className: "birthday-picker__mask",
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: "birthday-picker__sheet",
169
+ className: "date-picker__sheet",
170
170
  children: [
171
171
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
172
172
  id: titleId,
173
- className: "birthday-picker__title",
174
- children: "Дата рождения"
173
+ className: "date-picker__title",
174
+ children: title
175
175
  }),
176
176
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
177
- className: "birthday-picker__columns",
177
+ className: "date-picker__columns",
178
178
  children: [
179
179
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
180
- className: "birthday-picker__column",
180
+ className: "date-picker__column",
181
181
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
182
- className: "birthday-picker__caption",
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: "birthday-picker__column",
195
+ className: "date-picker__column",
196
196
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
197
- className: "birthday-picker__caption",
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: "birthday-picker__column",
210
+ className: "date-picker__column",
211
211
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
212
- className: "birthday-picker__caption",
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: "birthday-picker__actions",
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.BirthdayPicker = BirthdayPicker;
243
+ exports.DatePicker = DatePicker;
244
244
 
245
- //# sourceMappingURL=birthday-picker.cjs.map
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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"}
@@ -1,23 +1,26 @@
1
- export interface BirthdayPickerProps {
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 BirthdayPicker: ({ value, onChange, minYear, maxYear, minAge, disabled, name, placeholder, error, className }: BirthdayPickerProps) => import("react").JSX.Element;
23
- //# sourceMappingURL=birthday-picker.d.ts.map
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
@@ -1,23 +1,26 @@
1
- export interface BirthdayPickerProps {
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 BirthdayPicker: ({ value, onChange, minYear, maxYear, minAge, disabled, name, placeholder, error, className }: BirthdayPickerProps) => import("react").JSX.Element;
23
- //# sourceMappingURL=birthday-picker.d.cts.map
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 "./birthday-picker.scss";
12
- // src/components/birthday-picker/birthday-picker.tsx
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 formatBirthday = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
50
+ const formatDate = (date) => `${date.getDate()} ${MONTH_NAMES_GENITIVE[date.getMonth()]} ${date.getFullYear()}`;
51
51
  /** Сегодня минус `minAge` лет. 29 февраля в невисокосном году уходит на 28-е. */
52
- const maxBirthDate = (minAge, today) => {
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 BirthdayPicker = ({ value, onChange, minYear = 1930, maxYear, minAge = 18, disabled = false, name, placeholder, error, className }) => {
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 = maxBirthDate(minAge, today);
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("birthday-picker", className),
130
+ className: cn("date-picker", className),
131
131
  children: [
132
132
  /* @__PURE__ */ jsx(Input, {
133
133
  ref: triggerRef,
134
- className: "birthday-picker__trigger",
135
- value: value ? formatBirthday(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: placeholder ?? `Возраст от ${minAge} лет`,
144
+ placeholder,
145
145
  disabled,
146
146
  error
147
147
  }),
148
148
  /* @__PURE__ */ jsx(IconCalendar, {
149
- className: "birthday-picker__icon",
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: "birthday-picker__layer",
158
+ className: "date-picker__layer",
159
159
  children: [/* @__PURE__ */ jsx("button", {
160
160
  type: "button",
161
- className: "birthday-picker__mask",
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: "birthday-picker__sheet",
169
+ className: "date-picker__sheet",
170
170
  children: [
171
171
  /* @__PURE__ */ jsx("h2", {
172
172
  id: titleId,
173
- className: "birthday-picker__title",
174
- children: "Дата рождения"
173
+ className: "date-picker__title",
174
+ children: title
175
175
  }),
176
176
  /* @__PURE__ */ jsxs("div", {
177
- className: "birthday-picker__columns",
177
+ className: "date-picker__columns",
178
178
  children: [
179
179
  /* @__PURE__ */ jsxs("div", {
180
- className: "birthday-picker__column",
180
+ className: "date-picker__column",
181
181
  children: [/* @__PURE__ */ jsx("span", {
182
- className: "birthday-picker__caption",
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: "birthday-picker__column",
195
+ className: "date-picker__column",
196
196
  children: [/* @__PURE__ */ jsx("span", {
197
- className: "birthday-picker__caption",
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: "birthday-picker__column",
210
+ className: "date-picker__column",
211
211
  children: [/* @__PURE__ */ jsx("span", {
212
- className: "birthday-picker__caption",
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: "birthday-picker__actions",
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 { BirthdayPicker };
243
+ export { DatePicker };
244
244
 
245
- //# sourceMappingURL=birthday-picker.js.map
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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"}
@@ -1,14 +1,14 @@
1
- .birthday-picker {
1
+ .date-picker {
2
2
  position: relative;
3
3
  width: 100%;
4
4
 
5
- .birthday-picker__trigger {
5
+ .date-picker__trigger {
6
6
  cursor: pointer;
7
7
  padding-right: 45px;
8
8
  }
9
9
  }
10
10
 
11
- .birthday-picker__icon {
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
- .birthday-picker__layer {
19
+ .date-picker__layer {
20
20
  position: fixed;
21
21
  inset: 0;
22
22
  z-index: 1000;
23
23
  }
24
24
 
25
- .birthday-picker__mask {
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
- .birthday-picker__sheet {
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
- .birthday-picker__title {
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
- .birthday-picker__columns {
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
- .birthday-picker__column {
66
+ .date-picker__column {
67
67
  display: grid;
68
68
  gap: var(--space-1);
69
69
  min-width: 0;
70
70
  }
71
71
 
72
- .birthday-picker__caption {
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
- .birthday-picker__actions {
79
+ .date-picker__actions {
80
80
  display: flex;
81
81
  justify-content: space-between;
82
82
  align-items: center;
@@ -0,0 +1,2 @@
1
+ import { DatePicker, DatePickerProps } from "./date-picker.cjs";
2
+ export { DatePicker, type DatePickerProps };
@@ -0,0 +1,2 @@
1
+ import { DatePicker, DatePickerProps } from "./date-picker.js";
2
+ export { DatePicker, type DatePickerProps };
@@ -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-lg);
10
- font-weight: 900;
11
- line-height: var(--text-lg--line-height);
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;
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 require_birthday_picker = require("./components/birthday-picker/birthday-picker.cjs");
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 { BirthdayPicker, BirthdayPickerProps } from "./components/birthday-picker/birthday-picker.cjs";
10
- import "./components/birthday-picker/index.cjs";
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 { BirthdayPicker, type BirthdayPickerProps, Button, type ButtonAsChildProps, type ButtonComponentProps, type ButtonProps, type ButtonSize, type ButtonVariant, CheckboxInput, type CheckboxInputProps, 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 };
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 { BirthdayPicker, BirthdayPickerProps } from "./components/birthday-picker/birthday-picker.js";
10
- import "./components/birthday-picker/index.js";
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 { BirthdayPicker, type BirthdayPickerProps, Button, type ButtonAsChildProps, type ButtonComponentProps, type ButtonProps, type ButtonSize, type ButtonVariant, CheckboxInput, type CheckboxInputProps, 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 };
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 { BirthdayPicker } from "./components/birthday-picker/birthday-picker.js";
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 { BirthdayPicker, Button, CheckboxInput, Field, Form, FormActions, FormConsent, FormSubmit, HR, Input, PHONE_MASK, PhoneInput, RadioGroup, RadioInput, Spinner, TextArea };
13
+ export { Button, CheckboxInput, DatePicker, Field, Form, FormActions, FormConsent, FormSubmit, HR, Input, PHONE_MASK, PhoneInput, RadioGroup, RadioInput, Spinner, TextArea };
@@ -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
- }
@@ -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
+ }
@@ -48,9 +48,10 @@
48
48
  --radius-lg: 10px;
49
49
 
50
50
  --font-sans: 'Inter', sans-serif;
51
+ --font-weight-black: 900;
51
52
 
52
- // Шкала текста: размер и парная к нему высота строки. Других размеров в splittest нет,
53
- // по умолчанию почти везде --text-sm. Заголовки: h1 и h2 — 2xl (h1 с 680px — 3xl), h3 — lg, h4 — sm.
53
+ // Шкала текста в именах Tailwind: --text-<имя> и парная --text-<имя>--line-height.
54
+ // Размеры свои: 4xl — 45px, 3xl — 36px, 2xl — 27px, lg — 18px.
54
55
  --text-xs: 11px;
55
56
  --text-xs--line-height: 15px;
56
57
  --text-sm: 14px;
@@ -58,11 +59,15 @@
58
59
  --text-base: 16px;
59
60
  --text-base--line-height: 20px;
60
61
  --text-lg: 18px;
61
- --text-lg--line-height: 20px;
62
- --text-2xl: 28px;
62
+ --text-lg--line-height: 30px;
63
+ --text-xl: 18px;
64
+ --text-xl--line-height: 20px;
65
+ --text-2xl: 27px;
63
66
  --text-2xl--line-height: 30px;
64
- --text-3xl: 45px;
65
- --text-3xl--line-height: 50px;
67
+ --text-3xl: 36px;
68
+ --text-3xl--line-height: 40px;
69
+ --text-4xl: 45px;
70
+ --text-4xl--line-height: 50px;
66
71
 
67
72
  // Сетка 5px. Паддинги кнопки и инпута сняты с макета вне сетки и остаются литералами.
68
73
  --space-1: 5px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@de-kit/st-ui",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "React-компоненты splittest: глобальный SCSS и токены из основного фронтенда",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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":";;;;;;;;;;;;AAYA,MAAM,cAAc;CAClB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AASA,MAAM,eAAe,MAAc,UACjC,IAAI,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ;AAEnC,MAAM,WAAW,UAA+B;CAC9C,KAAK,KAAK,QAAQ;CAClB,OAAO,KAAK,SAAS,IAAI;CACzB,MAAM,KAAK,YAAY;AACzB;AAEA,MAAM,UAAU,EAAE,KAAK,OAAO,WAC5B,IAAI,KAAK,MAAM,QAAQ,GAAG,GAAG;AAE/B,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;AAE5D,MAAM,aAAa,SACjB,GAAG,KAAK,YAAY,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,EAAE,GAAG,IAAI,KAAK,QAAQ,CAAC;AAEzE,MAAM,kBAAkB,SACtB,GAAG,KAAK,QAAQ,EAAE,GAAG,qBAAqB,KAAK,SAAS,GAAG,GAAG,KAAK,YAAY;;AAGjF,MAAM,gBAAgB,QAAgB,UAAgB;CACpD,MAAM,OAAO,MAAM,YAAY,IAAI;CACnC,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,MAAM,KAAK,IAAI,MAAM,QAAQ,GAAG,YAAY,MAAM,QAAQ,CAAC,CAAC;CAClE,OAAO,IAAI,KAAK,MAAM,OAAO,GAAG;AAClC;;AAGA,MAAM,cAAc,OAAsB,QAA6B;CACrE,MAAM,OAAO;EACX,GAAG;EACH,KAAK,KAAK,IAAI,MAAM,KAAK,YAAY,MAAM,MAAM,MAAM,KAAK,CAAC;CAC/D;CAEA,OAAO,OAAO,IAAI,IAAI,MAAM,QAAQ,GAAG,IAAI;AAC7C;AAEA,MAAM,SAAS,MAAc,OAC3B,OAAO,KAAK,CAAC,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,OAAO,EAAE,IAAI,GAAG,MAAM,OAAO,CAAC;;;;;;AAwB3E,MAAa,kBAAkB,EAC7B,OACA,UACA,UAAU,MACV,SACA,SAAS,IACT,WAAW,OACX,MACA,aACA,OACA,gBACyB;CACzB,MAAM,CAAC,UAAA,GAASA,MAAAA,SAAAA,uBAAe,IAAI,KAAK,CAAC;CACzC,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;CACtC,MAAM,WAAW,aAAa,QAAQ,KAAK;CAC3C,MAAM,WAAW,KAAK,IACpB,WAAW,MAAM,YAAY,GAC7B,SAAS,YAAY,CACvB;CACA,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,OACxB,WAAW,QAAQ,SAAS,QAAQ,GAAG,QAAQ,CACjD;CACA,MAAM,cAAA,GAAaC,MAAAA,OAAAA,CAAyB,IAAI;CAChD,MAAM,YAAA,GAAWA,MAAAA,OAAAA,CAA0B,IAAI;CAC/C,MAAM,WAAA,GAAUC,MAAAA,MAAAA,CAAM;CAEtB,MAAM,QAAQ,MAAM,KAAK,IAAI,SAAS,MAAM,IAAI,GAAG,QAAQ;CAC3D,MAAM,OAAO,MAAM,GAAG,YAAY,MAAM,MAAM,MAAM,KAAK,CAAC;CAG1D,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,UAAU,KAAK,UAAU,SAAS,KAAK;EAC/C,MAAM,UAAU,OAAO;EACvB,SAAS,KAAK,MAAM,WAAW;EAC/B,SAAS,KAAK,MAAM,MAAM,IAAI,QAAQ;EACtC,SAAS,KAAK,MAAM,QAAQ;EAC5B,SAAS,SACL,cAA2B,uBAAqB,CAAC,EACjD,MAAM;EAEV,MAAM,iBAAiB,UAAoC;GACzD,IAAI,MAAM,QAAQ,UAAU;GAE5B,QAAQ,KAAK;GACb,WAAW,SAAS,MAAM;EAC5B;EACA,SAAS,iBAAiB,WAAW,aAAa;EAElD,aAAa;GACX,SAAS,oBAAoB,WAAW,aAAa;GACrD,SAAS,KAAK,MAAM,WAAW;GAC/B,SAAS,KAAK,MAAM,MAAM;GAC1B,SAAS,KAAK,MAAM,QAAQ;GAC5B,OAAO,SAAS,GAAG,OAAO;EAC5B;CACF,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,kBAAkB;EACtB,IAAI,UAAU;EAEd,SAAS,WAAW,QAAQ,SAAS,QAAQ,GAAG,QAAQ,CAAC;EACzD,QAAQ,IAAI;CACd;CAEA,MAAM,cAAc;EAClB,QAAQ,KAAK;EACb,WAAW,SAAS,MAAM;CAC5B;CAEA,MAAM,eAAe,UAAkC;EACrD,UAAS,SAAQ,WAAW;GAAE,GAAG;GAAM,GAAG;EAAM,GAAG,QAAQ,CAAC;CAC9D;CAEA,MAAM,cAAc;EAClB,WAAW,OAAO,KAAK,CAAC;EACxB,MAAM;CACR;CAGA,MAAM,wBAAwB,UAA2C;EACvE,IAAI,MAAM,QAAQ,OAAO;EAEzB,MAAM,eAAe;EACrB,IACE,MAAM,QAAQ,WACd,MAAM,QAAQ,OACd,MAAM,QAAQ,aAEd,UAAU;CAEd;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWC,WAAAA,GAAG,mBAAmB,SAAS;EAA/C,UAAA;GACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,cAAAA,OAAD;IACE,KAAK;IACL,WAAU;IACV,OAAO,QAAQ,eAAe,KAAK,IAAI;IACvC,gBAAgB,CAAC;IACjB,SAAS;IACT,WAAW;IACX,UAAS,UAAS,MAAM,eAAe;IACvC,WAAU;IACV,cAAa;IACb,iBAAc;IACd,iBAAe;IACf,aAAa,eAAe,cAAc,OAAO;IACvC;IACH;GACR,CAAA;GACD,iBAAA,GAAA,kBAAA,IAAA,CAACC,sBAAAA,cAAD;IAAc,WAAU;IAAwB,eAAA;GAAa,CAAA;GAC5D,OACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;IACE,MAAK;IACC;IACN,OAAO,QAAQ,UAAU,KAAK,IAAI;GACnC,CAAA,IACC;GAEH,QAAA,GACGC,UAAAA,aAAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,cAAW;KACX,SAAS;IACV,CAAA,GACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;KACE,KAAK;KACL,MAAA;KACA,cAAW;KACX,mBAAiB;KACjB,WAAU;KALZ,UAAA;MAOE,iBAAA,GAAA,kBAAA,IAAA,CAAC,MAAD;OAAI,IAAI;OAAS,WAAU;OAAyB,UAAA;MAEhD,CAAA;MACJ,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAU,CAAA,GACrD,iBAAA,GAAA,kBAAA,IAAA,CAACC,qBAAAA,aAAD;UACE,OAAM;UACN,OAAO,KAAK,KAAI,SAAQ;WACtB,OAAO;WACP,OAAO,OAAO,GAAG;UACnB,EAAE;UACF,OAAO,MAAM;UACb,WAAU,QAAO,YAAY,EAAE,IAAI,CAAC;SACrC,CAAA,CACE;;QACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAW,CAAA,GACtD,iBAAA,GAAA,kBAAA,IAAA,CAACA,qBAAAA,aAAD;UACE,OAAM;UACN,OAAO,YAAY,KAAK,OAAO,WAAW;WACxC,OAAO,QAAQ;WACf,OAAO;UACT,EAAE;UACF,OAAO,MAAM;UACb,WAAU,UAAS,YAAY,EAAE,MAAM,CAAC;SACzC,CAAA,CACE;;QACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAS,CAAA,GACpD,iBAAA,GAAA,kBAAA,IAAA,CAACA,qBAAAA,aAAD;UACE,OAAM;UACN,OAAO,MAAM,KAAI,UAAS;WACxB,OAAO;WACP,OAAO,OAAO,IAAI;UACpB,EAAE;UACF,OAAO,MAAM;UACb,WAAU,SAAQ,YAAY,EAAE,KAAK,CAAC;SACvC,CAAA,CACE;;OACF;;MACL,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAACC,eAAAA,QAAD;QAAQ,SAAS;QAAjB,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,qBAAAA,aAAD,CAAc,CAAA,GAAC,WAET;OACR,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAACD,eAAAA,QAAD;QAAQ,SAAQ;QAAO,SAAS;QAAO,UAAA;OAE/B,CAAA,CACL;;KACC;IACL,CAAA,CAAA;GACL,CAAA,GAAA,SAAS,IACX,IACA;EACD;;AAET"}
@@ -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":";;;;;;;;;;;;AAYA,MAAM,cAAc;CAClB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAGA,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AASA,MAAM,eAAe,MAAc,UACjC,IAAI,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ;AAEnC,MAAM,WAAW,UAA+B;CAC9C,KAAK,KAAK,QAAQ;CAClB,OAAO,KAAK,SAAS,IAAI;CACzB,MAAM,KAAK,YAAY;AACzB;AAEA,MAAM,UAAU,EAAE,KAAK,OAAO,WAC5B,IAAI,KAAK,MAAM,QAAQ,GAAG,GAAG;AAE/B,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;AAE5D,MAAM,aAAa,SACjB,GAAG,KAAK,YAAY,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,EAAE,GAAG,IAAI,KAAK,QAAQ,CAAC;AAEzE,MAAM,kBAAkB,SACtB,GAAG,KAAK,QAAQ,EAAE,GAAG,qBAAqB,KAAK,SAAS,GAAG,GAAG,KAAK,YAAY;;AAGjF,MAAM,gBAAgB,QAAgB,UAAgB;CACpD,MAAM,OAAO,MAAM,YAAY,IAAI;CACnC,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,MAAM,KAAK,IAAI,MAAM,QAAQ,GAAG,YAAY,MAAM,QAAQ,CAAC,CAAC;CAClE,OAAO,IAAI,KAAK,MAAM,OAAO,GAAG;AAClC;;AAGA,MAAM,cAAc,OAAsB,QAA6B;CACrE,MAAM,OAAO;EACX,GAAG;EACH,KAAK,KAAK,IAAI,MAAM,KAAK,YAAY,MAAM,MAAM,MAAM,KAAK,CAAC;CAC/D;CAEA,OAAO,OAAO,IAAI,IAAI,MAAM,QAAQ,GAAG,IAAI;AAC7C;AAEA,MAAM,SAAS,MAAc,OAC3B,OAAO,KAAK,CAAC,IAAI,MAAM,KAAK,EAAE,QAAQ,KAAK,OAAO,EAAE,IAAI,GAAG,MAAM,OAAO,CAAC;;;;;;AAwB3E,MAAa,kBAAkB,EAC7B,OACA,UACA,UAAU,MACV,SACA,SAAS,IACT,WAAW,OACX,MACA,aACA,OACA,gBACyB;CACzB,MAAM,CAAC,SAAS,+BAAe,IAAI,KAAK,CAAC;CACzC,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CACtC,MAAM,WAAW,aAAa,QAAQ,KAAK;CAC3C,MAAM,WAAW,KAAK,IACpB,WAAW,MAAM,YAAY,GAC7B,SAAS,YAAY,CACvB;CACA,MAAM,CAAC,OAAO,YAAY,eACxB,WAAW,QAAQ,SAAS,QAAQ,GAAG,QAAQ,CACjD;CACA,MAAM,aAAa,OAAyB,IAAI;CAChD,MAAM,WAAW,OAA0B,IAAI;CAC/C,MAAM,UAAU,MAAM;CAEtB,MAAM,QAAQ,MAAM,KAAK,IAAI,SAAS,MAAM,IAAI,GAAG,QAAQ;CAC3D,MAAM,OAAO,MAAM,GAAG,YAAY,MAAM,MAAM,MAAM,KAAK,CAAC;CAG1D,gBAAgB;EACd,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,UAAU,KAAK,UAAU,SAAS,KAAK;EAC/C,MAAM,UAAU,OAAO;EACvB,SAAS,KAAK,MAAM,WAAW;EAC/B,SAAS,KAAK,MAAM,MAAM,IAAI,QAAQ;EACtC,SAAS,KAAK,MAAM,QAAQ;EAC5B,SAAS,SACL,cAA2B,uBAAqB,CAAC,EACjD,MAAM;EAEV,MAAM,iBAAiB,UAAoC;GACzD,IAAI,MAAM,QAAQ,UAAU;GAE5B,QAAQ,KAAK;GACb,WAAW,SAAS,MAAM;EAC5B;EACA,SAAS,iBAAiB,WAAW,aAAa;EAElD,aAAa;GACX,SAAS,oBAAoB,WAAW,aAAa;GACrD,SAAS,KAAK,MAAM,WAAW;GAC/B,SAAS,KAAK,MAAM,MAAM;GAC1B,SAAS,KAAK,MAAM,QAAQ;GAC5B,OAAO,SAAS,GAAG,OAAO;EAC5B;CACF,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,kBAAkB;EACtB,IAAI,UAAU;EAEd,SAAS,WAAW,QAAQ,SAAS,QAAQ,GAAG,QAAQ,CAAC;EACzD,QAAQ,IAAI;CACd;CAEA,MAAM,cAAc;EAClB,QAAQ,KAAK;EACb,WAAW,SAAS,MAAM;CAC5B;CAEA,MAAM,eAAe,UAAkC;EACrD,UAAS,SAAQ,WAAW;GAAE,GAAG;GAAM,GAAG;EAAM,GAAG,QAAQ,CAAC;CAC9D;CAEA,MAAM,cAAc;EAClB,WAAW,OAAO,KAAK,CAAC;EACxB,MAAM;CACR;CAGA,MAAM,wBAAwB,UAA2C;EACvE,IAAI,MAAM,QAAQ,OAAO;EAEzB,MAAM,eAAe;EACrB,IACE,MAAM,QAAQ,WACd,MAAM,QAAQ,OACd,MAAM,QAAQ,aAEd,UAAU;CAEd;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,mBAAmB,SAAS;EAA/C,UAAA;GACE,oBAAC,OAAD;IACE,KAAK;IACL,WAAU;IACV,OAAO,QAAQ,eAAe,KAAK,IAAI;IACvC,gBAAgB,CAAC;IACjB,SAAS;IACT,WAAW;IACX,UAAS,UAAS,MAAM,eAAe;IACvC,WAAU;IACV,cAAa;IACb,iBAAc;IACd,iBAAe;IACf,aAAa,eAAe,cAAc,OAAO;IACvC;IACH;GACR,CAAA;GACD,oBAAC,cAAD;IAAc,WAAU;IAAwB,eAAA;GAAa,CAAA;GAC5D,OACC,oBAAC,SAAD;IACE,MAAK;IACC;IACN,OAAO,QAAQ,UAAU,KAAK,IAAI;GACnC,CAAA,IACC;GAEH,OACG,aACE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,cAAW;KACX,SAAS;IACV,CAAA,GACD,qBAAC,UAAD;KACE,KAAK;KACL,MAAA;KACA,cAAW;KACX,mBAAiB;KACjB,WAAU;KALZ,UAAA;MAOE,oBAAC,MAAD;OAAI,IAAI;OAAS,WAAU;OAAyB,UAAA;MAEhD,CAAA;MACJ,qBAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,qBAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,oBAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAU,CAAA,GACrD,oBAAC,aAAD;UACE,OAAM;UACN,OAAO,KAAK,KAAI,SAAQ;WACtB,OAAO;WACP,OAAO,OAAO,GAAG;UACnB,EAAE;UACF,OAAO,MAAM;UACb,WAAU,QAAO,YAAY,EAAE,IAAI,CAAC;SACrC,CAAA,CACE;;QACL,qBAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,oBAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAW,CAAA,GACtD,oBAAC,aAAD;UACE,OAAM;UACN,OAAO,YAAY,KAAK,OAAO,WAAW;WACxC,OAAO,QAAQ;WACf,OAAO;UACT,EAAE;UACF,OAAO,MAAM;UACb,WAAU,UAAS,YAAY,EAAE,MAAM,CAAC;SACzC,CAAA,CACE;;QACL,qBAAC,OAAD;SAAK,WAAU;SAAf,UAAA,CACE,oBAAC,QAAD;UAAM,WAAU;UAA2B,UAAA;SAAS,CAAA,GACpD,oBAAC,aAAD;UACE,OAAM;UACN,OAAO,MAAM,KAAI,UAAS;WACxB,OAAO;WACP,OAAO,OAAO,IAAI;UACpB,EAAE;UACF,OAAO,MAAM;UACb,WAAU,SAAQ,YAAY,EAAE,KAAK,CAAC;SACvC,CAAA,CACE;;OACF;;MACL,qBAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,qBAAC,QAAD;QAAQ,SAAS;QAAjB,UAAA,CACE,oBAAC,aAAD,CAAc,CAAA,GAAC,WAET;OACR,CAAA,GAAA,oBAAC,QAAD;QAAQ,SAAQ;QAAO,SAAS;QAAO,UAAA;OAE/B,CAAA,CACL;;KACC;IACL,CAAA,CAAA;GACL,CAAA,GAAA,SAAS,IACX,IACA;EACD;;AAET"}
@@ -1,2 +0,0 @@
1
- import { BirthdayPicker, BirthdayPickerProps } from "./birthday-picker.cjs";
2
- export { BirthdayPicker, type BirthdayPickerProps };
@@ -1,2 +0,0 @@
1
- import { BirthdayPicker, BirthdayPickerProps } from "./birthday-picker.js";
2
- export { BirthdayPicker, type BirthdayPickerProps };
@@ -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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"}