@de-kit/st-ui 0.1.11 → 0.1.14

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 (63) hide show
  1. package/README.md +2 -2
  2. package/dist/components/birthday-picker/birthday-picker.cjs +245 -0
  3. package/dist/components/birthday-picker/birthday-picker.cjs.map +1 -0
  4. package/dist/components/birthday-picker/birthday-picker.d.cts +24 -0
  5. package/dist/components/birthday-picker/birthday-picker.d.ts +24 -0
  6. package/dist/components/birthday-picker/birthday-picker.js +245 -0
  7. package/dist/components/birthday-picker/birthday-picker.js.map +1 -0
  8. package/dist/components/birthday-picker/birthday-picker.scss +84 -0
  9. package/dist/components/birthday-picker/index.d.cts +2 -0
  10. package/dist/components/birthday-picker/index.d.ts +2 -0
  11. package/dist/components/birthday-picker/wheel-column.cjs +114 -0
  12. package/dist/components/birthday-picker/wheel-column.cjs.map +1 -0
  13. package/dist/components/birthday-picker/wheel-column.js +114 -0
  14. package/dist/components/birthday-picker/wheel-column.js.map +1 -0
  15. package/dist/components/birthday-picker/wheel-column.scss +61 -0
  16. package/dist/components/button/button.scss +2 -2
  17. package/dist/components/field/field.cjs +4 -3
  18. package/dist/components/field/field.cjs.map +1 -1
  19. package/dist/components/field/field.d.cts +1 -1
  20. package/dist/components/field/field.d.ts +1 -1
  21. package/dist/components/field/field.js +4 -3
  22. package/dist/components/field/field.js.map +1 -1
  23. package/dist/components/form/form.cjs +124 -0
  24. package/dist/components/form/form.cjs.map +1 -0
  25. package/dist/components/form/form.d.cts +31 -0
  26. package/dist/components/form/form.d.ts +31 -0
  27. package/dist/components/form/form.js +121 -0
  28. package/dist/components/form/form.js.map +1 -0
  29. package/dist/components/form/form.scss +29 -0
  30. package/dist/components/form/index.d.cts +2 -0
  31. package/dist/components/form/index.d.ts +2 -0
  32. package/dist/components/phone-input/index.d.cts +2 -0
  33. package/dist/components/phone-input/index.d.ts +2 -0
  34. package/dist/components/phone-input/phone-input.cjs +105 -0
  35. package/dist/components/phone-input/phone-input.cjs.map +1 -0
  36. package/dist/components/phone-input/phone-input.d.cts +29 -0
  37. package/dist/components/phone-input/phone-input.d.ts +29 -0
  38. package/dist/components/phone-input/phone-input.js +103 -0
  39. package/dist/components/phone-input/phone-input.js.map +1 -0
  40. package/dist/icons/icon-gallery-next.cjs +1 -1
  41. package/dist/icons/icon-gallery-next.cjs.map +1 -1
  42. package/dist/icons/icon-gallery-next.js +1 -1
  43. package/dist/icons/icon-gallery-next.js.map +1 -1
  44. package/dist/icons/icon-gallery-prev.cjs +1 -1
  45. package/dist/icons/icon-gallery-prev.cjs.map +1 -1
  46. package/dist/icons/icon-gallery-prev.js +1 -1
  47. package/dist/icons/icon-gallery-prev.js.map +1 -1
  48. package/dist/icons/icon-label-ozon.cjs +4 -4
  49. package/dist/icons/icon-label-ozon.cjs.map +1 -1
  50. package/dist/icons/icon-label-ozon.js +4 -4
  51. package/dist/icons/icon-label-ozon.js.map +1 -1
  52. package/dist/icons/icon-label-yandex.cjs +1 -1
  53. package/dist/icons/icon-label-yandex.cjs.map +1 -1
  54. package/dist/icons/icon-label-yandex.js +1 -1
  55. package/dist/icons/icon-label-yandex.js.map +1 -1
  56. package/dist/icons/index.cjs +2 -2
  57. package/dist/icons/index.js +2 -2
  58. package/dist/index.cjs +10 -0
  59. package/dist/index.d.cts +7 -1
  60. package/dist/index.d.ts +7 -1
  61. package/dist/index.js +4 -1
  62. package/dist/styles/vars.scss +2 -2
  63. package/package.json +8 -5
@@ -0,0 +1,84 @@
1
+ .birthday-picker {
2
+ position: relative;
3
+ width: 100%;
4
+
5
+ .birthday-picker__trigger {
6
+ cursor: pointer;
7
+ padding-right: 44px;
8
+ }
9
+ }
10
+
11
+ .birthday-picker__icon {
12
+ position: absolute;
13
+ top: 50%;
14
+ right: 14px;
15
+ transform: translateY(-50%);
16
+ pointer-events: none;
17
+ }
18
+
19
+ .birthday-picker__layer {
20
+ position: fixed;
21
+ inset: 0;
22
+ z-index: 1000;
23
+ }
24
+
25
+ .birthday-picker__mask {
26
+ position: absolute;
27
+ inset: 0;
28
+ padding: 0;
29
+ border: 0;
30
+ background: rgb(0 0 0 / 40%);
31
+ cursor: pointer;
32
+ }
33
+
34
+ .birthday-picker__sheet {
35
+ box-sizing: border-box;
36
+ position: absolute;
37
+ right: 0;
38
+ bottom: 0;
39
+ left: 0;
40
+ width: 100%;
41
+ max-width: none;
42
+ height: auto;
43
+ margin: 0;
44
+ border: 0;
45
+ padding: var(--space-4) var(--space-4) var(--space-6);
46
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
47
+ background: var(--color-background);
48
+ color: var(--color-foreground);
49
+ box-shadow: var(--shadow-xl);
50
+ }
51
+
52
+ .birthday-picker__title {
53
+ margin: 0 0 var(--space-3);
54
+ font-size: var(--text-sm);
55
+ font-weight: 700;
56
+ text-align: center;
57
+ text-transform: uppercase;
58
+ }
59
+
60
+ .birthday-picker__columns {
61
+ display: grid;
62
+ grid-template-columns: 1fr 1.5fr 1fr;
63
+ gap: var(--space-2);
64
+ }
65
+
66
+ .birthday-picker__column {
67
+ display: grid;
68
+ gap: var(--space-1);
69
+ min-width: 0;
70
+ }
71
+
72
+ .birthday-picker__caption {
73
+ font-size: var(--text-xs);
74
+ color: var(--color-muted-foreground);
75
+ text-align: center;
76
+ text-transform: uppercase;
77
+ }
78
+
79
+ .birthday-picker__actions {
80
+ display: flex;
81
+ justify-content: space-between;
82
+ align-items: center;
83
+ margin-top: var(--space-4);
84
+ }
@@ -0,0 +1,2 @@
1
+ import { BirthdayPicker, BirthdayPickerProps } from "./birthday-picker.cjs";
2
+ export { BirthdayPicker, type BirthdayPickerProps };
@@ -0,0 +1,2 @@
1
+ import { BirthdayPicker, BirthdayPickerProps } from "./birthday-picker.js";
2
+ export { BirthdayPicker, type BirthdayPickerProps };
@@ -0,0 +1,114 @@
1
+ const require_cn = require("../../lib/cn.cjs");
2
+ let react = require("react");
3
+ let react_jsx_runtime = require("react/jsx-runtime");
4
+ require("./wheel-column.scss");
5
+ const VISIBLE_ROWS = 5;
6
+ /** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */
7
+ const SETTLE_DELAY = 120;
8
+ const clamp = (index, max) => Math.min(Math.max(index, 0), max);
9
+ /**
10
+ * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.
11
+ * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.
12
+ */
13
+ const WheelColumn = ({ label, items, value, onChange, className }) => {
14
+ const listRef = (0, react.useRef)(null);
15
+ const settleTimer = (0, react.useRef)(void 0);
16
+ const frame = (0, react.useRef)(void 0);
17
+ const scrolling = (0, react.useRef)(false);
18
+ const latest = (0, react.useRef)({
19
+ items,
20
+ value,
21
+ onChange
22
+ });
23
+ (0, react.useEffect)(() => {
24
+ latest.current = {
25
+ items,
26
+ value,
27
+ onChange
28
+ };
29
+ });
30
+ const index = Math.max(0, items.findIndex((item) => item.value === value));
31
+ const selected = items[index];
32
+ (0, react.useEffect)(() => {
33
+ const node = listRef.current;
34
+ if (!node || scrolling.current) return;
35
+ const target = index * 40;
36
+ if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;
37
+ }, [index, items.length]);
38
+ (0, react.useEffect)(() => () => {
39
+ window.clearTimeout(settleTimer.current);
40
+ if (frame.current !== void 0) window.cancelAnimationFrame(frame.current);
41
+ }, []);
42
+ const reportPosition = () => {
43
+ frame.current = void 0;
44
+ const node = listRef.current;
45
+ const { items: list, value: current, onChange: emit } = latest.current;
46
+ if (!node || list.length === 0) return;
47
+ const item = list[clamp(Math.round(node.scrollTop / 40), list.length - 1)];
48
+ if (item && item.value !== current) emit(item.value);
49
+ };
50
+ const handleScroll = () => {
51
+ scrolling.current = true;
52
+ window.clearTimeout(settleTimer.current);
53
+ settleTimer.current = window.setTimeout(() => {
54
+ scrolling.current = false;
55
+ }, SETTLE_DELAY);
56
+ if (frame.current === void 0) frame.current = window.requestAnimationFrame(reportPosition);
57
+ };
58
+ const select = (next) => {
59
+ const item = items[next];
60
+ if (!item || next === index) return;
61
+ onChange(item.value);
62
+ };
63
+ const handleKeyDown = (event) => {
64
+ const next = {
65
+ ArrowUp: index - 1,
66
+ ArrowDown: index + 1,
67
+ PageUp: index - VISIBLE_ROWS,
68
+ PageDown: index + VISIBLE_ROWS,
69
+ Home: 0,
70
+ End: items.length - 1
71
+ }[event.key];
72
+ if (next === void 0) return;
73
+ event.preventDefault();
74
+ select(clamp(next, items.length - 1));
75
+ };
76
+ const spacer = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
77
+ className: "wheel__spacer",
78
+ style: { height: 40 * Math.floor(VISIBLE_ROWS / 2) },
79
+ "aria-hidden": true
80
+ });
81
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
82
+ className: "wheel-frame",
83
+ style: { "--wheel-item-height": `40px` },
84
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
85
+ ref: listRef,
86
+ role: "spinbutton",
87
+ "aria-label": label,
88
+ "aria-valuemin": items[0]?.value,
89
+ "aria-valuemax": items.at(-1)?.value,
90
+ "aria-valuenow": selected?.value,
91
+ "aria-valuetext": selected?.label,
92
+ tabIndex: 0,
93
+ className: require_cn.cn("wheel", className),
94
+ style: { height: 200 },
95
+ onScroll: handleScroll,
96
+ onKeyDown: handleKeyDown,
97
+ children: [
98
+ spacer,
99
+ items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
100
+ type: "button",
101
+ tabIndex: -1,
102
+ className: require_cn.cn("wheel__item", require_cn.when(i === index, "wheel__item--active")),
103
+ style: { height: 40 },
104
+ onClick: () => select(i),
105
+ children: item.label
106
+ }, item.value)),
107
+ spacer
108
+ ]
109
+ })
110
+ });
111
+ };
112
+ exports.WheelColumn = WheelColumn;
113
+
114
+ //# sourceMappingURL=wheel-column.cjs.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,114 @@
1
+ import { cn, when } from "../../lib/cn.js";
2
+ import { useEffect, useRef } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import "./wheel-column.scss";
5
+ const VISIBLE_ROWS = 5;
6
+ /** Пауза после последнего scroll-события, по которой считаем колесо остановленным. */
7
+ const SETTLE_DELAY = 120;
8
+ const clamp = (index, max) => Math.min(Math.max(index, 0), max);
9
+ /**
10
+ * Колесо с прокруткой и snap. Выбранный пункт — тот, что посередине окна.
11
+ * Колесо мыши, тач и стрелки меняют значение, клик по пункту выбирает его.
12
+ */
13
+ const WheelColumn = ({ label, items, value, onChange, className }) => {
14
+ const listRef = useRef(null);
15
+ const settleTimer = useRef(void 0);
16
+ const frame = useRef(void 0);
17
+ const scrolling = useRef(false);
18
+ const latest = useRef({
19
+ items,
20
+ value,
21
+ onChange
22
+ });
23
+ useEffect(() => {
24
+ latest.current = {
25
+ items,
26
+ value,
27
+ onChange
28
+ };
29
+ });
30
+ const index = Math.max(0, items.findIndex((item) => item.value === value));
31
+ const selected = items[index];
32
+ useEffect(() => {
33
+ const node = listRef.current;
34
+ if (!node || scrolling.current) return;
35
+ const target = index * 40;
36
+ if (Math.abs(node.scrollTop - target) > 1) node.scrollTop = target;
37
+ }, [index, items.length]);
38
+ useEffect(() => () => {
39
+ window.clearTimeout(settleTimer.current);
40
+ if (frame.current !== void 0) window.cancelAnimationFrame(frame.current);
41
+ }, []);
42
+ const reportPosition = () => {
43
+ frame.current = void 0;
44
+ const node = listRef.current;
45
+ const { items: list, value: current, onChange: emit } = latest.current;
46
+ if (!node || list.length === 0) return;
47
+ const item = list[clamp(Math.round(node.scrollTop / 40), list.length - 1)];
48
+ if (item && item.value !== current) emit(item.value);
49
+ };
50
+ const handleScroll = () => {
51
+ scrolling.current = true;
52
+ window.clearTimeout(settleTimer.current);
53
+ settleTimer.current = window.setTimeout(() => {
54
+ scrolling.current = false;
55
+ }, SETTLE_DELAY);
56
+ if (frame.current === void 0) frame.current = window.requestAnimationFrame(reportPosition);
57
+ };
58
+ const select = (next) => {
59
+ const item = items[next];
60
+ if (!item || next === index) return;
61
+ onChange(item.value);
62
+ };
63
+ const handleKeyDown = (event) => {
64
+ const next = {
65
+ ArrowUp: index - 1,
66
+ ArrowDown: index + 1,
67
+ PageUp: index - VISIBLE_ROWS,
68
+ PageDown: index + VISIBLE_ROWS,
69
+ Home: 0,
70
+ End: items.length - 1
71
+ }[event.key];
72
+ if (next === void 0) return;
73
+ event.preventDefault();
74
+ select(clamp(next, items.length - 1));
75
+ };
76
+ const spacer = /* @__PURE__ */ jsx("div", {
77
+ className: "wheel__spacer",
78
+ style: { height: 40 * Math.floor(VISIBLE_ROWS / 2) },
79
+ "aria-hidden": true
80
+ });
81
+ return /* @__PURE__ */ jsx("div", {
82
+ className: "wheel-frame",
83
+ style: { "--wheel-item-height": `40px` },
84
+ children: /* @__PURE__ */ jsxs("div", {
85
+ ref: listRef,
86
+ role: "spinbutton",
87
+ "aria-label": label,
88
+ "aria-valuemin": items[0]?.value,
89
+ "aria-valuemax": items.at(-1)?.value,
90
+ "aria-valuenow": selected?.value,
91
+ "aria-valuetext": selected?.label,
92
+ tabIndex: 0,
93
+ className: cn("wheel", className),
94
+ style: { height: 200 },
95
+ onScroll: handleScroll,
96
+ onKeyDown: handleKeyDown,
97
+ children: [
98
+ spacer,
99
+ items.map((item, i) => /* @__PURE__ */ jsx("button", {
100
+ type: "button",
101
+ tabIndex: -1,
102
+ className: cn("wheel__item", when(i === index, "wheel__item--active")),
103
+ style: { height: 40 },
104
+ onClick: () => select(i),
105
+ children: item.label
106
+ }, item.value)),
107
+ spacer
108
+ ]
109
+ })
110
+ });
111
+ };
112
+ export { WheelColumn };
113
+
114
+ //# sourceMappingURL=wheel-column.js.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,61 @@
1
+ .wheel-frame {
2
+ position: relative;
3
+ }
4
+
5
+ // Полоса выбранного пункта посередине окна. Поверх колеса, не перехватывает клики.
6
+ .wheel-frame::after {
7
+ content: '';
8
+ position: absolute;
9
+ top: 50%;
10
+ right: 0;
11
+ left: 0;
12
+ height: var(--wheel-item-height);
13
+ transform: translateY(-50%);
14
+ border-top: 1px solid var(--color-border);
15
+ border-bottom: 1px solid var(--color-border);
16
+ pointer-events: none;
17
+ }
18
+
19
+ .wheel {
20
+ overflow-y: auto;
21
+ overscroll-behavior: contain;
22
+ scroll-snap-type: y mandatory;
23
+ scrollbar-width: none;
24
+ font-size: var(--text-sm);
25
+ outline: none;
26
+
27
+ &::-webkit-scrollbar {
28
+ display: none;
29
+ }
30
+
31
+ &:focus-visible {
32
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
33
+ border-radius: var(--radius-sm);
34
+ }
35
+ }
36
+
37
+ .wheel__spacer {
38
+ flex: none;
39
+ pointer-events: none;
40
+ }
41
+
42
+ .wheel__item {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: 100%;
47
+ margin: 0;
48
+ padding: 0;
49
+ border: 0;
50
+ background: none;
51
+ color: var(--color-muted-foreground);
52
+ font: inherit;
53
+ scroll-snap-align: center;
54
+ cursor: pointer;
55
+ transition: color 0.15s;
56
+ }
57
+
58
+ .wheel__item--active {
59
+ color: var(--color-foreground);
60
+ font-weight: 700;
61
+ }
@@ -5,7 +5,7 @@
5
5
  width: fit-content;
6
6
  display: flex;
7
7
  align-items: center;
8
- padding: 8px 18px;
8
+ padding: 8px 16px;
9
9
  border-radius: 30px;
10
10
  font-size: var(--text-sm);
11
11
  line-height: var(--text-sm--line-height);
@@ -18,7 +18,7 @@
18
18
  flex-shrink: 0;
19
19
  cursor: pointer;
20
20
  transition: none;
21
- gap: 8px;
21
+ gap: 5px;
22
22
  text-decoration: none;
23
23
 
24
24
  &:hover:not(:active, [data-active], :disabled, [aria-disabled='true']) {
@@ -12,7 +12,8 @@ const requiredText = (label, message) => {
12
12
  };
13
13
  const Field = ({ label, hint, hintPlacement = "bottom", error, required = false, requiredMessage, className, children }) => {
14
14
  const inputId = (0, react.useId)();
15
- const labelId = `${inputId}-label`;
15
+ const hasLabel = Boolean(label);
16
+ const labelId = hasLabel ? `${inputId}-label` : void 0;
16
17
  const rootRef = (0, react.useRef)(null);
17
18
  const hadValueRef = (0, react.useRef)(false);
18
19
  const [showRequired, setShowRequired] = (0, react.useState)(false);
@@ -79,12 +80,12 @@ const Field = ({ label, hint, hintPlacement = "bottom", error, required = false,
79
80
  ref: rootRef,
80
81
  className: require_cn.cn("field", className, require_cn.when(Boolean(hint) && hintPlacement === "label", "field--hint-label")),
81
82
  children: [
82
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
83
+ hasLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
83
84
  id: labelId,
84
85
  className: "field__label",
85
86
  htmlFor: inputId,
86
87
  children: label
87
- }),
88
+ }) : null,
88
89
  hintByPlacement.label,
89
90
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_field_context.FieldControlProvider, {
90
91
  value: contextValue,
@@ -1 +1 @@
1
- {"version":3,"file":"field.cjs","names":["useId","useRef","useState","useCallback","useMemo","cn","when","FieldControlProvider"],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const labelId = `${inputId}-label`;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":";;;;;;AAwCA,MAAM,YAAY,UAAkB,MAAM,KAAK,MAAM;AAErD,MAAM,gBAAgB,OAAkB,YAAmC;CACzE,IAAI,SAAS,OAAO;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;CACrD,OAAO;AACT;AAEA,MAAa,SAAS,EACpB,OACA,MACA,gBAAgB,UAChB,OACA,WAAW,OACX,iBACA,WACA,eACgB;CAChB,MAAM,WAAA,GAAUA,MAAAA,MAAAA,CAAM;CACtB,MAAM,UAAU,GAAG,QAAQ;CAC3B,MAAM,WAAA,GAAUC,MAAAA,OAAAA,CAAuB,IAAI;CAG3C,MAAM,eAAA,GAAcA,MAAAA,OAAAA,CAAO,KAAK;CAChC,MAAM,CAAC,cAAc,oBAAA,GAAmBC,MAAAA,SAAAA,CAAS,KAAK;CACtD,MAAM,gBACJ,YAAY,eAAe,aAAa,OAAO,eAAe,IAAI;CACpE,MAAM,eAAe,SAAS;CAC9B,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,OAAO,GAAG,QAAQ,SAAS,KAAA;CAC1C,MAAM,UAAU,UAAU,GAAG,QAAQ,UAAU,KAAA;CAC/C,MAAM,cAAc,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE9D,MAAM,eAAA,GAAcC,MAAAA,YAAAA,EACjB,UAAkB;EACjB,IAAI,CAAC,UAAU;EAEf,IAAI,SAAS,KAAK,GAAG;GACnB,YAAY,UAAU;GACtB,gBAAgB,KAAK;GACrB;EACF;EAEA,IAAI,YAAY,SAAS,gBAAgB,IAAI;CAC/C,GACA,CAAC,QAAQ,CACX;;;;;;;;;CAUA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,CAAC,UAAU;EAExB,MAAM,iBAAiB,UAAiB;GACtC,MAAM,SAAS,MAAM;GAOrB,KALG,kBAAkB,oBACjB,kBAAkB,uBAClB,kBAAkB,sBACpB,OAAO,SAAS,cAEL,gBAAgB,IAAI;EACnC;EAEA,KAAK,iBAAiB,WAAW,eAAe,IAAI;EACpD,aAAa,KAAK,oBAAoB,WAAW,eAAe,IAAI;CACtE,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,gBAAA,GAAeC,MAAAA,QAAAA,QACZ;EACL,IAAI;EACJ;EACA;EACA;EACA,aAAa,eAAe,KAAA;EAC5B;EACA;CACF,IACA;EAAC;EAAa;EAAS;EAAS;EAAS;EAAS;EAAa;CAAQ,CACzE;CAEA,MAAM,kBAAyD;EAC7D,OAAO;EACP,QAAQ;CACV;CAEA,IAAI,MACF,gBAAgB,iBACd,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;EACE,IAAI;EACJ,WAAWC,WAAAA,GACT,eACAC,WAAAA,KAAK,kBAAkB,SAAS,oBAAoB,CACtD;EAEC,UAAA;CACA,CAAA;CAIP,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EACE,KAAK;EACL,WAAWD,WAAAA,GACT,SACA,WACAC,WAAAA,KAAK,QAAQ,IAAI,KAAK,kBAAkB,SAAS,mBAAmB,CACtE;EANF,UAAA;GAQE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;IAAO,IAAI;IAAS,WAAU;IAAe,SAAS;IACnD,UAAA;GACI,CAAA;GACN,gBAAgB;GACjB,iBAAA,GAAA,kBAAA,IAAA,CAACC,sBAAAA,sBAAD;IAAsB,OAAO;IAC1B;GACmB,CAAA;GACrB,eACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAAG,IAAI;IAAS,WAAU;IAAe,MAAK;IAC3C,UAAA;GACA,CAAA,IACD;GACH,gBAAgB;EACd;;AAET"}
1
+ {"version":3,"file":"field.cjs","names":["useId","useRef","useState","useCallback","useMemo","cn","when","FieldControlProvider"],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label?: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const hasLabel = Boolean(label);\n const labelId = hasLabel ? `${inputId}-label` : undefined;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n {hasLabel ? (\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from "react";
2
2
  export type FieldHintPlacement = 'bottom' | 'label';
3
3
  export interface FieldProps {
4
- label: ReactNode;
4
+ label?: ReactNode;
5
5
  hint?: ReactNode;
6
6
  /** `bottom` — под полем, `label` — сразу под лейблом. */
7
7
  hintPlacement?: FieldHintPlacement;
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from "react";
2
2
  export type FieldHintPlacement = 'bottom' | 'label';
3
3
  export interface FieldProps {
4
- label: ReactNode;
4
+ label?: ReactNode;
5
5
  hint?: ReactNode;
6
6
  /** `bottom` — под полем, `label` — сразу под лейблом. */
7
7
  hintPlacement?: FieldHintPlacement;
@@ -12,7 +12,8 @@ const requiredText = (label, message) => {
12
12
  };
13
13
  const Field = ({ label, hint, hintPlacement = "bottom", error, required = false, requiredMessage, className, children }) => {
14
14
  const inputId = useId();
15
- const labelId = `${inputId}-label`;
15
+ const hasLabel = Boolean(label);
16
+ const labelId = hasLabel ? `${inputId}-label` : void 0;
16
17
  const rootRef = useRef(null);
17
18
  const hadValueRef = useRef(false);
18
19
  const [showRequired, setShowRequired] = useState(false);
@@ -79,12 +80,12 @@ const Field = ({ label, hint, hintPlacement = "bottom", error, required = false,
79
80
  ref: rootRef,
80
81
  className: cn("field", className, when(Boolean(hint) && hintPlacement === "label", "field--hint-label")),
81
82
  children: [
82
- /* @__PURE__ */ jsx("label", {
83
+ hasLabel ? /* @__PURE__ */ jsx("label", {
83
84
  id: labelId,
84
85
  className: "field__label",
85
86
  htmlFor: inputId,
86
87
  children: label
87
- }),
88
+ }) : null,
88
89
  hintByPlacement.label,
89
90
  /* @__PURE__ */ jsx(FieldControlProvider, {
90
91
  value: contextValue,
@@ -1 +1 @@
1
- {"version":3,"file":"field.js","names":[],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const labelId = `${inputId}-label`;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":";;;;;;AAwCA,MAAM,YAAY,UAAkB,MAAM,KAAK,MAAM;AAErD,MAAM,gBAAgB,OAAkB,YAAmC;CACzE,IAAI,SAAS,OAAO;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;CACrD,OAAO;AACT;AAEA,MAAa,SAAS,EACpB,OACA,MACA,gBAAgB,UAChB,OACA,WAAW,OACX,iBACA,WACA,eACgB;CAChB,MAAM,UAAU,MAAM;CACtB,MAAM,UAAU,GAAG,QAAQ;CAC3B,MAAM,UAAU,OAAuB,IAAI;CAG3C,MAAM,cAAc,OAAO,KAAK;CAChC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,gBACJ,YAAY,eAAe,aAAa,OAAO,eAAe,IAAI;CACpE,MAAM,eAAe,SAAS;CAC9B,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,OAAO,GAAG,QAAQ,SAAS,KAAA;CAC1C,MAAM,UAAU,UAAU,GAAG,QAAQ,UAAU,KAAA;CAC/C,MAAM,cAAc,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE9D,MAAM,cAAc,aACjB,UAAkB;EACjB,IAAI,CAAC,UAAU;EAEf,IAAI,SAAS,KAAK,GAAG;GACnB,YAAY,UAAU;GACtB,gBAAgB,KAAK;GACrB;EACF;EAEA,IAAI,YAAY,SAAS,gBAAgB,IAAI;CAC/C,GACA,CAAC,QAAQ,CACX;;;;;;;;;CAUA,gBAAgB;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,CAAC,UAAU;EAExB,MAAM,iBAAiB,UAAiB;GACtC,MAAM,SAAS,MAAM;GAOrB,KALG,kBAAkB,oBACjB,kBAAkB,uBAClB,kBAAkB,sBACpB,OAAO,SAAS,cAEL,gBAAgB,IAAI;EACnC;EAEA,KAAK,iBAAiB,WAAW,eAAe,IAAI;EACpD,aAAa,KAAK,oBAAoB,WAAW,eAAe,IAAI;CACtE,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,eAAe,eACZ;EACL,IAAI;EACJ;EACA;EACA;EACA,aAAa,eAAe,KAAA;EAC5B;EACA;CACF,IACA;EAAC;EAAa;EAAS;EAAS;EAAS;EAAS;EAAa;CAAQ,CACzE;CAEA,MAAM,kBAAyD;EAC7D,OAAO;EACP,QAAQ;CACV;CAEA,IAAI,MACF,gBAAgB,iBACd,oBAAC,KAAD;EACE,IAAI;EACJ,WAAW,GACT,eACA,KAAK,kBAAkB,SAAS,oBAAoB,CACtD;EAEC,UAAA;CACA,CAAA;CAIP,OACE,qBAAC,OAAD;EACE,KAAK;EACL,WAAW,GACT,SACA,WACA,KAAK,QAAQ,IAAI,KAAK,kBAAkB,SAAS,mBAAmB,CACtE;EANF,UAAA;GAQE,oBAAC,SAAD;IAAO,IAAI;IAAS,WAAU;IAAe,SAAS;IACnD,UAAA;GACI,CAAA;GACN,gBAAgB;GACjB,oBAAC,sBAAD;IAAsB,OAAO;IAC1B;GACmB,CAAA;GACrB,eACC,oBAAC,KAAD;IAAG,IAAI;IAAS,WAAU;IAAe,MAAK;IAC3C,UAAA;GACA,CAAA,IACD;GACH,gBAAgB;EACd;;AAET"}
1
+ {"version":3,"file":"field.js","names":[],"sources":["../../../src/components/field/field.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport {\n FieldControlProvider,\n type FieldControlContext,\n} from './field-context';\nimport './field.scss';\n\nexport type FieldHintPlacement = 'bottom' | 'label';\n\nexport interface FieldProps {\n label?: ReactNode;\n hint?: ReactNode;\n /** `bottom` — под полем, `label` — сразу под лейблом. */\n hintPlacement?: FieldHintPlacement;\n error?: ReactNode;\n /** Пустое значение после того, как в поле уже что-то было. */\n required?: boolean;\n /**\n * По умолчанию «Поле «{label}» обязательно», если лейбл — строка.\n * Слово «поле» среднего рода, поэтому формулировка не зависит от рода лейбла.\n */\n requiredMessage?: ReactNode;\n className?: string;\n /**\n * Один контрол: id он берёт у поля, поэтому свой `id` на ребёнке\n * перебивается, а два контрола внутри получат одинаковый.\n */\n children: ReactNode;\n}\n\nconst isFilled = (value: string) => value.trim() !== '';\n\nconst requiredText = (label: ReactNode, message: ReactNode | undefined) => {\n if (message) return message;\n if (typeof label === 'string') return `Поле «${label}» обязательно`;\n return 'Обязательное поле';\n};\n\nexport const Field = ({\n label,\n hint,\n hintPlacement = 'bottom',\n error,\n required = false,\n requiredMessage,\n className,\n children,\n}: FieldProps) => {\n const inputId = useId();\n const hasLabel = Boolean(label);\n const labelId = hasLabel ? `${inputId}-label` : undefined;\n const rootRef = useRef<HTMLDivElement>(null);\n // ponytail: value/defaultValue больше не читаем с ребёнка. Контрол зовёт\n // reportValue из эффекта — до первого эффекта hadValue ещё false.\n const hadValueRef = useRef(false);\n const [showRequired, setShowRequired] = useState(false);\n const requiredError =\n required && showRequired ? requiredText(label, requiredMessage) : null;\n const visibleError = error ?? requiredError;\n const invalid = Boolean(visibleError);\n const hintId = hint ? `${inputId}-hint` : undefined;\n const errorId = invalid ? `${inputId}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(' ');\n\n const reportValue = useCallback(\n (value: string) => {\n if (!required) return;\n\n if (isFilled(value)) {\n hadValueRef.current = true;\n setShowRequired(false);\n return;\n }\n\n if (hadValueRef.current) setShowRequired(true);\n },\n [required],\n );\n\n /**\n * Второй повод пожаловаться — неудачная проверка формы. Нужен для контролов,\n * которые нельзя опустошить: радиогруппу обратно не снимут, и до отправки\n * момента для ошибки нет. `invalid` не всплывает, но проходит фазу захвата,\n * поэтому одного слушателя на корне поля хватает на любой контрол внутри.\n * Реагируем только на пустое значение: формат, длина и прочие нарушения\n * обязательности не касаются, и чужую ошибку выдавать за неё нельзя.\n */\n useEffect(() => {\n const node = rootRef.current;\n if (!node || !required) return;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n const missing =\n (target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement) &&\n target.validity.valueMissing;\n\n if (missing) setShowRequired(true);\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, [required]);\n\n const contextValue = useMemo<FieldControlContext>(\n () => ({\n id: inputId,\n labelId,\n invalid,\n required,\n describedBy: describedBy || undefined,\n errorId,\n reportValue,\n }),\n [describedBy, errorId, inputId, invalid, labelId, reportValue, required],\n );\n\n const hintByPlacement: Record<FieldHintPlacement, ReactNode> = {\n label: null,\n bottom: null,\n };\n\n if (hint) {\n hintByPlacement[hintPlacement] = (\n <p\n id={hintId}\n className={cn(\n 'field__hint',\n when(hintPlacement === 'label', 'field__hint--label'),\n )}\n >\n {hint}\n </p>\n );\n }\n\n return (\n <div\n ref={rootRef}\n className={cn(\n 'field',\n className,\n when(Boolean(hint) && hintPlacement === 'label', 'field--hint-label'),\n )}\n >\n {hasLabel ? (\n <label id={labelId} className=\"field__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n {hintByPlacement.label}\n <FieldControlProvider value={contextValue}>\n {children}\n </FieldControlProvider>\n {visibleError ? (\n <p id={errorId} className=\"field__error\" role=\"alert\">\n {visibleError}\n </p>\n ) : null}\n {hintByPlacement.bottom}\n </div>\n );\n};\n"],"mappings":";;;;;;AAwCA,MAAM,YAAY,UAAkB,MAAM,KAAK,MAAM;AAErD,MAAM,gBAAgB,OAAkB,YAAmC;CACzE,IAAI,SAAS,OAAO;CACpB,IAAI,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;CACrD,OAAO;AACT;AAEA,MAAa,SAAS,EACpB,OACA,MACA,gBAAgB,UAChB,OACA,WAAW,OACX,iBACA,WACA,eACgB;CAChB,MAAM,UAAU,MAAM;CACtB,MAAM,WAAW,QAAQ,KAAK;CAC9B,MAAM,UAAU,WAAW,GAAG,QAAQ,UAAU,KAAA;CAChD,MAAM,UAAU,OAAuB,IAAI;CAG3C,MAAM,cAAc,OAAO,KAAK;CAChC,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,gBACJ,YAAY,eAAe,aAAa,OAAO,eAAe,IAAI;CACpE,MAAM,eAAe,SAAS;CAC9B,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,OAAO,GAAG,QAAQ,SAAS,KAAA;CAC1C,MAAM,UAAU,UAAU,GAAG,QAAQ,UAAU,KAAA;CAC/C,MAAM,cAAc,CAAC,SAAS,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE9D,MAAM,cAAc,aACjB,UAAkB;EACjB,IAAI,CAAC,UAAU;EAEf,IAAI,SAAS,KAAK,GAAG;GACnB,YAAY,UAAU;GACtB,gBAAgB,KAAK;GACrB;EACF;EAEA,IAAI,YAAY,SAAS,gBAAgB,IAAI;CAC/C,GACA,CAAC,QAAQ,CACX;;;;;;;;;CAUA,gBAAgB;EACd,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,QAAQ,CAAC,UAAU;EAExB,MAAM,iBAAiB,UAAiB;GACtC,MAAM,SAAS,MAAM;GAOrB,KALG,kBAAkB,oBACjB,kBAAkB,uBAClB,kBAAkB,sBACpB,OAAO,SAAS,cAEL,gBAAgB,IAAI;EACnC;EAEA,KAAK,iBAAiB,WAAW,eAAe,IAAI;EACpD,aAAa,KAAK,oBAAoB,WAAW,eAAe,IAAI;CACtE,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,eAAe,eACZ;EACL,IAAI;EACJ;EACA;EACA;EACA,aAAa,eAAe,KAAA;EAC5B;EACA;CACF,IACA;EAAC;EAAa;EAAS;EAAS;EAAS;EAAS;EAAa;CAAQ,CACzE;CAEA,MAAM,kBAAyD;EAC7D,OAAO;EACP,QAAQ;CACV;CAEA,IAAI,MACF,gBAAgB,iBACd,oBAAC,KAAD;EACE,IAAI;EACJ,WAAW,GACT,eACA,KAAK,kBAAkB,SAAS,oBAAoB,CACtD;EAEC,UAAA;CACA,CAAA;CAIP,OACE,qBAAC,OAAD;EACE,KAAK;EACL,WAAW,GACT,SACA,WACA,KAAK,QAAQ,IAAI,KAAK,kBAAkB,SAAS,mBAAmB,CACtE;EANF,UAAA;GAQG,WACC,oBAAC,SAAD;IAAO,IAAI;IAAS,WAAU;IAAe,SAAS;IACnD,UAAA;GACI,CAAA,IACL;GACH,gBAAgB;GACjB,oBAAC,sBAAD;IAAsB,OAAO;IAC1B;GACmB,CAAA;GACrB,eACC,oBAAC,KAAD;IAAG,IAAI;IAAS,WAAU;IAAe,MAAK;IAC3C,UAAA;GACA,CAAA,IACD;GACH,gBAAgB;EACd;;AAET"}