@de-kit/st-ui 0.1.13 → 0.1.15

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 (46) hide show
  1. package/dist/components/birthday-picker/birthday-picker.cjs +245 -0
  2. package/dist/components/birthday-picker/birthday-picker.cjs.map +1 -0
  3. package/dist/components/birthday-picker/birthday-picker.d.cts +23 -0
  4. package/dist/components/birthday-picker/birthday-picker.d.ts +23 -0
  5. package/dist/components/birthday-picker/birthday-picker.js +245 -0
  6. package/dist/components/birthday-picker/birthday-picker.js.map +1 -0
  7. package/dist/components/birthday-picker/birthday-picker.scss +84 -0
  8. package/dist/components/birthday-picker/index.d.cts +2 -0
  9. package/dist/components/birthday-picker/index.d.ts +2 -0
  10. package/dist/components/birthday-picker/wheel-column.cjs +114 -0
  11. package/dist/components/birthday-picker/wheel-column.cjs.map +1 -0
  12. package/dist/components/birthday-picker/wheel-column.js +114 -0
  13. package/dist/components/birthday-picker/wheel-column.js.map +1 -0
  14. package/dist/components/birthday-picker/wheel-column.scss +61 -0
  15. package/dist/components/form/form.cjs +124 -0
  16. package/dist/components/form/form.cjs.map +1 -0
  17. package/dist/components/form/form.d.cts +31 -0
  18. package/dist/components/form/form.d.ts +31 -0
  19. package/dist/components/form/form.js +121 -0
  20. package/dist/components/form/form.js.map +1 -0
  21. package/dist/components/form/form.scss +29 -0
  22. package/dist/components/form/index.d.cts +2 -0
  23. package/dist/components/form/index.d.ts +2 -0
  24. package/dist/components/phone-input/index.d.cts +2 -0
  25. package/dist/components/phone-input/index.d.ts +2 -0
  26. package/dist/components/phone-input/phone-input.cjs +105 -0
  27. package/dist/components/phone-input/phone-input.cjs.map +1 -0
  28. package/dist/components/phone-input/phone-input.d.cts +28 -0
  29. package/dist/components/phone-input/phone-input.d.ts +28 -0
  30. package/dist/components/phone-input/phone-input.js +103 -0
  31. package/dist/components/phone-input/phone-input.js.map +1 -0
  32. package/dist/icons/icon-label-ozon.cjs +4 -4
  33. package/dist/icons/icon-label-ozon.cjs.map +1 -1
  34. package/dist/icons/icon-label-ozon.js +4 -4
  35. package/dist/icons/icon-label-ozon.js.map +1 -1
  36. package/dist/icons/icon-label-yandex.cjs +1 -1
  37. package/dist/icons/icon-label-yandex.cjs.map +1 -1
  38. package/dist/icons/icon-label-yandex.js +1 -1
  39. package/dist/icons/icon-label-yandex.js.map +1 -1
  40. package/dist/icons/index.cjs +2 -2
  41. package/dist/icons/index.js +2 -2
  42. package/dist/index.cjs +10 -0
  43. package/dist/index.d.cts +7 -1
  44. package/dist/index.d.ts +7 -1
  45. package/dist/index.js +4 -1
  46. 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
+ }
@@ -0,0 +1,124 @@
1
+ const require_cn = require("../../lib/cn.cjs");
2
+ const require_button = require("../button/button.cjs");
3
+ let react = require("react");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ require("./form.scss");
6
+ // src/components/form/form.tsx
7
+ const FormPendingContext = (0, react.createContext)(false);
8
+ const useFormPending = () => (0, react.useContext)(FormPendingContext);
9
+ const isThenable = (value) => (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";
10
+ const toMessage = (reason) => {
11
+ if (typeof reason === "string" && reason.trim()) return reason;
12
+ if (typeof reason === "object" && reason !== null && "message" in reason && typeof reason.message === "string" && reason.message.trim()) return reason.message;
13
+ return "Не получилось отправить. Попробуйте ещё раз";
14
+ };
15
+ const Form = (0, react.forwardRef)(function Form({ children, className, error, onSubmit, pending = false, ...props }, ref) {
16
+ const [busy, setBusy] = (0, react.useState)(false);
17
+ const [failure, setFailure] = (0, react.useState)(null);
18
+ const formNode = (0, react.useRef)(null);
19
+ const busyRef = (0, react.useRef)(false);
20
+ const requestId = (0, react.useRef)(0);
21
+ const live = (0, react.useRef)(true);
22
+ const submitting = pending || busy;
23
+ const shownError = error ?? failure;
24
+ (0, react.useEffect)(() => {
25
+ live.current = true;
26
+ return () => {
27
+ live.current = false;
28
+ };
29
+ }, []);
30
+ (0, react.useEffect)(() => {
31
+ const node = formNode.current;
32
+ if (!node) return;
33
+ let firstInvalid = null;
34
+ const handleInvalid = (event) => {
35
+ const target = event.target;
36
+ if (!(target instanceof HTMLElement) || !target.closest(".field")) return;
37
+ event.preventDefault();
38
+ if (firstInvalid) return;
39
+ firstInvalid = target;
40
+ queueMicrotask(() => {
41
+ firstInvalid?.focus();
42
+ firstInvalid = null;
43
+ });
44
+ };
45
+ node.addEventListener("invalid", handleInvalid, true);
46
+ return () => node.removeEventListener("invalid", handleInvalid, true);
47
+ }, []);
48
+ const setNode = (0, react.useCallback)((node) => {
49
+ formNode.current = node;
50
+ if (typeof ref === "function") {
51
+ ref(node);
52
+ return;
53
+ }
54
+ if (ref) ref.current = node;
55
+ }, [ref]);
56
+ const handleSubmit = (event) => {
57
+ if (!onSubmit) return;
58
+ event.preventDefault();
59
+ if (busyRef.current || pending) return;
60
+ const data = new FormData(event.currentTarget);
61
+ setFailure(null);
62
+ let result;
63
+ try {
64
+ result = onSubmit(data, event);
65
+ } catch (reason) {
66
+ setFailure(toMessage(reason));
67
+ return;
68
+ }
69
+ if (!isThenable(result)) return;
70
+ const id = ++requestId.current;
71
+ busyRef.current = true;
72
+ setBusy(true);
73
+ const finish = () => {
74
+ if (!live.current || requestId.current !== id) return;
75
+ busyRef.current = false;
76
+ setBusy(false);
77
+ };
78
+ Promise.resolve(result).then(() => {
79
+ finish();
80
+ }, (reason) => {
81
+ finish();
82
+ if (!live.current || requestId.current !== id) return;
83
+ setFailure(toMessage(reason));
84
+ });
85
+ };
86
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
87
+ ...props,
88
+ ref: setNode,
89
+ className: require_cn.cn("form", className),
90
+ onSubmit: handleSubmit,
91
+ "aria-busy": submitting || void 0,
92
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FormPendingContext.Provider, {
93
+ value: submitting,
94
+ children: [children, shownError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
95
+ className: "form__error",
96
+ role: "alert",
97
+ children: shownError
98
+ }) : null]
99
+ })
100
+ });
101
+ });
102
+ const FormActions = ({ children, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
103
+ className: require_cn.cn("form__buttons", className),
104
+ children
105
+ });
106
+ const FormConsent = ({ children, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
107
+ className: require_cn.cn("form__consent", className),
108
+ children
109
+ });
110
+ const FormSubmit = ({ loading = false, children, ...props }) => {
111
+ const pending = useFormPending();
112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_button.Button, {
113
+ ...props,
114
+ type: "submit",
115
+ loading: loading || pending,
116
+ children
117
+ });
118
+ };
119
+ exports.Form = Form;
120
+ exports.FormActions = FormActions;
121
+ exports.FormConsent = FormConsent;
122
+ exports.FormSubmit = FormSubmit;
123
+
124
+ //# sourceMappingURL=form.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.cjs","names":["createContext","useContext","forwardRef","useState","useRef","useCallback","cn","Button"],"sources":["../../../src/components/form/form.tsx"],"sourcesContent":["import {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n type FormEvent,\n type FormHTMLAttributes,\n type ForwardedRef,\n type ReactNode,\n} from 'react';\n\nimport { Button, type ButtonProps } from '@/components/button';\nimport { cn } from '@/lib/cn';\nimport './form.scss';\n\nconst FormPendingContext = createContext(false);\n\nconst useFormPending = () => useContext(FormPendingContext);\n\nconst isThenable = (value: unknown): value is PromiseLike<void> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n 'then' in value &&\n typeof value.then === 'function';\n\nconst toMessage = (reason: unknown) => {\n if (typeof reason === 'string' && reason.trim()) return reason;\n // Error и объекты вида `{ message }` из API-клиентов.\n if (\n typeof reason === 'object' &&\n reason !== null &&\n 'message' in reason &&\n typeof reason.message === 'string' &&\n reason.message.trim()\n ) {\n return reason.message;\n }\n return 'Не получилось отправить. Попробуйте ещё раз';\n};\n\nexport interface FormProps extends Omit<\n FormHTMLAttributes<HTMLFormElement>,\n 'onSubmit'\n> {\n /**\n * Вызывается после нативной проверки. `FormData` — по `name` контролов.\n * Промис держит кнопку, пока не осядет. Синхронный throw и reject — ошибка под полями.\n */\n onSubmit?: (\n data: FormData,\n event: FormEvent<HTMLFormElement>,\n ) => void | Promise<void>;\n /**\n * Запрос снаружи формы: `onSubmit` ничего не вернул, крутилку держит стор.\n * Складывается с ожиданием промиса.\n */\n pending?: boolean;\n /** Пока задана — она вместо текста из упавшего `onSubmit`. */\n error?: ReactNode;\n}\n\nexport interface FormActionsProps {\n children: ReactNode;\n className?: string;\n}\n\nexport interface FormConsentProps {\n /** Текст рядом с кнопкой: оферта, согласие. Ссылки можно класть внутрь. */\n children: ReactNode;\n className?: string;\n}\n\nexport const Form = forwardRef(function Form(\n {\n children,\n className,\n error,\n onSubmit,\n pending = false,\n ...props\n }: FormProps,\n ref: ForwardedRef<HTMLFormElement>,\n) {\n const [busy, setBusy] = useState(false);\n const [failure, setFailure] = useState<string | null>(null);\n const formNode = useRef<HTMLFormElement | null>(null);\n const busyRef = useRef(false);\n const requestId = useRef(0);\n const live = useRef(true);\n const submitting = pending || busy;\n const shownError = error ?? failure;\n\n useEffect(() => {\n live.current = true;\n return () => {\n live.current = false;\n };\n }, []);\n\n useEffect(() => {\n const node = formNode.current;\n if (!node) return;\n\n // Бабл браузера английский и перекрывает подсказку. Field уже рисует\n // ошибку. Отмена `invalid` убирает контрол из списка, которому браузер\n // показывает бабл. Сама проверка остаётся отрицательной, сабмит не уходит.\n let firstInvalid: HTMLElement | null = null;\n\n const handleInvalid = (event: Event) => {\n const target = event.target;\n if (!(target instanceof HTMLElement) || !target.closest('.field')) return;\n\n event.preventDefault();\n if (firstInvalid) return;\n\n // Браузер фокусит только контролы, чей `invalid` не отменён.\n // Первый в дереве — тот, с которого он начал бы сам.\n firstInvalid = target;\n queueMicrotask(() => {\n firstInvalid?.focus();\n firstInvalid = null;\n });\n };\n\n node.addEventListener('invalid', handleInvalid, true);\n return () => node.removeEventListener('invalid', handleInvalid, true);\n }, []);\n\n // Стабильная ссылка: иначе React на каждом рендере отцепляет и вешает ref заново.\n const setNode = useCallback(\n (node: HTMLFormElement | null) => {\n formNode.current = node;\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n if (ref) ref.current = node;\n },\n [ref],\n );\n\n const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n // Без своего onSubmit остаётся нативный action/method.\n if (!onSubmit) return;\n\n event.preventDefault();\n // Кнопка заблокирована, но requestSubmit и Enter её обходят.\n if (busyRef.current || pending) return;\n\n const data = new FormData(event.currentTarget);\n setFailure(null);\n\n let result: void | Promise<void>;\n try {\n result = onSubmit(data, event);\n } catch (reason) {\n setFailure(toMessage(reason));\n return;\n }\n\n if (!isThenable(result)) return;\n\n const id = ++requestId.current;\n busyRef.current = true;\n setBusy(true);\n\n const finish = () => {\n if (!live.current || requestId.current !== id) return;\n busyRef.current = false;\n setBusy(false);\n };\n\n void Promise.resolve(result).then(\n () => {\n finish();\n },\n (reason: unknown) => {\n finish();\n if (!live.current || requestId.current !== id) return;\n setFailure(toMessage(reason));\n },\n );\n };\n\n return (\n <form\n {...props}\n ref={setNode}\n className={cn('form', className)}\n onSubmit={handleSubmit}\n aria-busy={submitting || undefined}\n >\n <FormPendingContext.Provider value={submitting}>\n {children}\n {shownError ? (\n <p className=\"form__error\" role=\"alert\">\n {shownError}\n </p>\n ) : null}\n </FormPendingContext.Provider>\n </form>\n );\n});\n\nexport const FormActions = ({ children, className }: FormActionsProps) => (\n <div className={cn('form__buttons', className)}>{children}</div>\n);\n\nexport const FormConsent = ({ children, className }: FormConsentProps) => (\n <div className={cn('form__consent', className)}>{children}</div>\n);\n\nexport const FormSubmit = ({\n loading = false,\n children,\n ...props\n}: Omit<ButtonProps, 'type' | 'asChild'>) => {\n const pending = useFormPending();\n\n return (\n <Button {...props} type=\"submit\" loading={loading || pending}>\n {children}\n </Button>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { ButtonProps } from "../button/button.cjs";
2
+ import "../button/index.cjs";
3
+ import { FormEvent, FormHTMLAttributes, ReactNode } from "react";
4
+ export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'> {
5
+ /**
6
+ * Вызывается после нативной проверки. `FormData` — по `name` контролов.
7
+ * Промис держит кнопку, пока не осядет. Синхронный throw и reject — ошибка под полями.
8
+ */
9
+ onSubmit?: (data: FormData, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
10
+ /**
11
+ * Запрос снаружи формы: `onSubmit` ничего не вернул, крутилку держит стор.
12
+ * Складывается с ожиданием промиса.
13
+ */
14
+ pending?: boolean;
15
+ /** Пока задана — она вместо текста из упавшего `onSubmit`. */
16
+ error?: ReactNode;
17
+ }
18
+ export interface FormActionsProps {
19
+ children: ReactNode;
20
+ className?: string;
21
+ }
22
+ export interface FormConsentProps {
23
+ /** Текст рядом с кнопкой: оферта, согласие. Ссылки можно класть внутрь. */
24
+ children: ReactNode;
25
+ className?: string;
26
+ }
27
+ export declare const Form: import("react").ForwardRefExoticComponent<FormProps & import("react").RefAttributes<HTMLFormElement>>;
28
+ export declare const FormActions: ({ children, className }: FormActionsProps) => import("react").JSX.Element;
29
+ export declare const FormConsent: ({ children, className }: FormConsentProps) => import("react").JSX.Element;
30
+ export declare const FormSubmit: ({ loading, children, ...props }: Omit<ButtonProps, "type" | "asChild">) => import("react").JSX.Element;
31
+ //# sourceMappingURL=form.d.cts.map
@@ -0,0 +1,31 @@
1
+ import { ButtonProps } from "../button/button.js";
2
+ import "../button/index.js";
3
+ import { FormEvent, FormHTMLAttributes, ReactNode } from "react";
4
+ export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'> {
5
+ /**
6
+ * Вызывается после нативной проверки. `FormData` — по `name` контролов.
7
+ * Промис держит кнопку, пока не осядет. Синхронный throw и reject — ошибка под полями.
8
+ */
9
+ onSubmit?: (data: FormData, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
10
+ /**
11
+ * Запрос снаружи формы: `onSubmit` ничего не вернул, крутилку держит стор.
12
+ * Складывается с ожиданием промиса.
13
+ */
14
+ pending?: boolean;
15
+ /** Пока задана — она вместо текста из упавшего `onSubmit`. */
16
+ error?: ReactNode;
17
+ }
18
+ export interface FormActionsProps {
19
+ children: ReactNode;
20
+ className?: string;
21
+ }
22
+ export interface FormConsentProps {
23
+ /** Текст рядом с кнопкой: оферта, согласие. Ссылки можно класть внутрь. */
24
+ children: ReactNode;
25
+ className?: string;
26
+ }
27
+ export declare const Form: import("react").ForwardRefExoticComponent<FormProps & import("react").RefAttributes<HTMLFormElement>>;
28
+ export declare const FormActions: ({ children, className }: FormActionsProps) => import("react").JSX.Element;
29
+ export declare const FormConsent: ({ children, className }: FormConsentProps) => import("react").JSX.Element;
30
+ export declare const FormSubmit: ({ loading, children, ...props }: Omit<ButtonProps, "type" | "asChild">) => import("react").JSX.Element;
31
+ //# sourceMappingURL=form.d.ts.map