@de-kit/st-ui 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -17
- package/dist/components/button/button.cjs +37 -17
- package/dist/components/button/button.cjs.map +1 -1
- package/dist/components/button/button.d.cts +7 -2
- package/dist/components/button/button.d.ts +7 -2
- package/dist/components/button/button.js +37 -17
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/button.scss +65 -27
- package/dist/components/checkbox/checkbox.cjs +50 -8
- package/dist/components/checkbox/checkbox.cjs.map +1 -1
- package/dist/components/checkbox/checkbox.d.cts +11 -4
- package/dist/components/checkbox/checkbox.d.ts +11 -4
- package/dist/components/checkbox/checkbox.js +51 -9
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox/checkbox.scss +55 -26
- package/dist/components/field/field-context.cjs +9 -0
- package/dist/components/field/field-context.cjs.map +1 -0
- package/dist/components/field/field-context.js +8 -0
- package/dist/components/field/field-context.js.map +1 -0
- package/dist/components/field/field.cjs +105 -0
- package/dist/components/field/field.cjs.map +1 -0
- package/dist/components/field/field.d.cts +24 -0
- package/dist/components/field/field.d.ts +24 -0
- package/dist/components/field/field.js +105 -0
- package/dist/components/field/field.js.map +1 -0
- package/dist/components/field/field.scss +43 -0
- package/dist/components/field/index.d.cts +2 -0
- package/dist/components/field/index.d.ts +2 -0
- package/dist/components/input/index.d.cts +2 -2
- package/dist/components/input/index.d.ts +2 -2
- package/dist/components/input/input.cjs +88 -8
- package/dist/components/input/input.cjs.map +1 -1
- package/dist/components/input/input.d.cts +8 -1
- package/dist/components/input/input.d.ts +8 -1
- package/dist/components/input/input.js +90 -10
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.scss +77 -14
- package/dist/components/radio/index.d.cts +2 -2
- package/dist/components/radio/index.d.ts +2 -2
- package/dist/components/radio/radio.cjs +47 -5
- package/dist/components/radio/radio.cjs.map +1 -1
- package/dist/components/radio/radio.d.cts +12 -2
- package/dist/components/radio/radio.d.ts +12 -2
- package/dist/components/radio/radio.js +48 -7
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/radio/radio.scss +50 -15
- package/dist/components/spinner/spinner.cjs +38 -16
- package/dist/components/spinner/spinner.cjs.map +1 -1
- package/dist/components/spinner/spinner.d.cts +5 -3
- package/dist/components/spinner/spinner.d.ts +5 -3
- package/dist/components/spinner/spinner.js +39 -17
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/spinner/spinner.scss +18 -1
- package/dist/components/textarea/index.d.cts +2 -2
- package/dist/components/textarea/index.d.ts +2 -2
- package/dist/components/textarea/textarea.cjs +68 -9
- package/dist/components/textarea/textarea.cjs.map +1 -1
- package/dist/components/textarea/textarea.d.cts +4 -2
- package/dist/components/textarea/textarea.d.ts +4 -2
- package/dist/components/textarea/textarea.js +69 -10
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/textarea/textarea.scss +50 -23
- package/dist/icons/index.cjs +2 -2
- package/dist/icons/index.js +2 -2
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/styles/global.scss +33 -27
- package/dist/styles/mixins.scss +37 -174
- package/dist/styles/vars.scss +73 -22
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.field {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.field__label {
|
|
8
|
+
color: var(--color-foreground);
|
|
9
|
+
font-size: var(--text-lg);
|
|
10
|
+
font-weight: 900;
|
|
11
|
+
line-height: var(--text-lg--line-height);
|
|
12
|
+
display: block;
|
|
13
|
+
margin-bottom: var(--space-3);
|
|
14
|
+
overflow-wrap: anywhere;
|
|
15
|
+
-webkit-hyphens: auto;
|
|
16
|
+
hyphens: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.field__hint,
|
|
20
|
+
.field__error {
|
|
21
|
+
margin: var(--space-2) 0 0 0;
|
|
22
|
+
font-size: var(--text-xs);
|
|
23
|
+
line-height: var(--text-xs--line-height);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.field__hint {
|
|
27
|
+
color: var(--color-muted-foreground);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.field--hint-label .field__label {
|
|
31
|
+
margin-bottom: var(--space-2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.field__hint--label {
|
|
35
|
+
margin: 0 0 var(--space-3);
|
|
36
|
+
font-size: var(--text-sm);
|
|
37
|
+
line-height: var(--text-sm--line-height);
|
|
38
|
+
color: var(--color-foreground);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.field__error {
|
|
42
|
+
color: var(--color-destructive);
|
|
43
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Input, InputProps } from "./input.cjs";
|
|
2
|
-
export { Input, type InputProps };
|
|
1
|
+
import { Input, InputProps, InputSize } from "./input.cjs";
|
|
2
|
+
export { Input, type InputProps, type InputSize };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Input, InputProps } from "./input.js";
|
|
2
|
-
export { Input, type InputProps };
|
|
1
|
+
import { Input, InputProps, InputSize } from "./input.js";
|
|
2
|
+
export { Input, type InputProps, type InputSize };
|
|
@@ -1,16 +1,96 @@
|
|
|
1
1
|
const require_cn = require("../../lib/cn.cjs");
|
|
2
|
+
const require_field_context = require("../field/field-context.cjs");
|
|
3
|
+
const require_icon_cross = require("../../icons/icon-cross.cjs");
|
|
2
4
|
let react = require("react");
|
|
3
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
6
|
require("./input.scss");
|
|
5
7
|
// src/components/input/input.tsx
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
ref
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const assignRef = (ref, node) => {
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
ref(node);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (ref) ref.current = node;
|
|
14
|
+
};
|
|
15
|
+
const toInputValue = (value) => value == null ? "" : String(value);
|
|
16
|
+
/**
|
|
17
|
+
* Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.
|
|
18
|
+
* Сеттер прототипа трекер не трогает.
|
|
19
|
+
*/
|
|
20
|
+
const emitInputValue = (input, next) => {
|
|
21
|
+
(Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set)?.call(input, next);
|
|
22
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
23
|
+
};
|
|
24
|
+
const Input = (0, react.forwardRef)(function Input({ type = "text", size = "md", recommended, error, clearable = false, clearLabel = "Очистить", onClear, className, disabled, readOnly, value, defaultValue, onChange, ...props }, ref) {
|
|
25
|
+
const field = require_field_context.useFieldControl();
|
|
26
|
+
const reportValue = field?.reportValue;
|
|
27
|
+
const inputRef = (0, react.useRef)(null);
|
|
28
|
+
const isControlled = value !== void 0;
|
|
29
|
+
const [uncontrolledValue, setUncontrolledValue] = (0, react.useState)(() => toInputValue(defaultValue));
|
|
30
|
+
const current = isControlled ? toInputValue(value) : uncontrolledValue;
|
|
31
|
+
const isInvalid = Boolean(error) || Boolean(field?.invalid);
|
|
32
|
+
const describedBy = [field?.describedBy, props["aria-describedby"]].filter((part) => Boolean(part)).join(" ");
|
|
33
|
+
const setRef = (0, react.useCallback)((node) => {
|
|
34
|
+
inputRef.current = node;
|
|
35
|
+
assignRef(ref, node);
|
|
36
|
+
}, [ref]);
|
|
37
|
+
(0, react.useEffect)(() => {
|
|
38
|
+
const form = inputRef.current?.form;
|
|
39
|
+
if (!form || isControlled) return;
|
|
40
|
+
const handleReset = () => {
|
|
41
|
+
setUncontrolledValue(toInputValue(defaultValue));
|
|
42
|
+
};
|
|
43
|
+
form.addEventListener("reset", handleReset);
|
|
44
|
+
return () => form.removeEventListener("reset", handleReset);
|
|
45
|
+
}, [isControlled, defaultValue]);
|
|
46
|
+
(0, react.useEffect)(() => {
|
|
47
|
+
reportValue?.(current);
|
|
48
|
+
}, [current, reportValue]);
|
|
49
|
+
const handleChange = (event) => {
|
|
50
|
+
if (!isControlled) setUncontrolledValue(event.target.value);
|
|
51
|
+
onChange?.(event);
|
|
52
|
+
};
|
|
53
|
+
const handleClearMouseDown = (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
};
|
|
56
|
+
const handleClear = () => {
|
|
57
|
+
const input = inputRef.current;
|
|
58
|
+
if (!input) return;
|
|
59
|
+
emitInputValue(input, "");
|
|
60
|
+
onClear?.();
|
|
61
|
+
input.focus();
|
|
62
|
+
};
|
|
63
|
+
const canClear = clearable && !disabled && !readOnly && current !== "";
|
|
64
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
65
|
+
className: "input__wrap",
|
|
66
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
67
|
+
...props,
|
|
68
|
+
ref: setRef,
|
|
69
|
+
type,
|
|
70
|
+
disabled,
|
|
71
|
+
readOnly,
|
|
72
|
+
...isControlled ? { value } : { defaultValue },
|
|
73
|
+
onChange: handleChange,
|
|
74
|
+
...isInvalid ? { "aria-invalid": true } : {},
|
|
75
|
+
...field ? { id: field.id } : {},
|
|
76
|
+
...field?.required ? {
|
|
77
|
+
"required": true,
|
|
78
|
+
"aria-required": true
|
|
79
|
+
} : {},
|
|
80
|
+
...describedBy ? { "aria-describedby": describedBy } : {},
|
|
81
|
+
...field?.errorId ? { "aria-errormessage": field.errorId } : {},
|
|
82
|
+
className: require_cn.cn("input", className, require_cn.when(size !== "md", `input--${size}`), require_cn.when(recommended, "input--recommended"), require_cn.when(isInvalid, "input--error"), require_cn.when(clearable && !disabled && !readOnly, "input--clearable"))
|
|
83
|
+
}), canClear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
84
|
+
type: "button",
|
|
85
|
+
className: "input__clear",
|
|
86
|
+
"aria-label": clearLabel,
|
|
87
|
+
onMouseDown: handleClearMouseDown,
|
|
88
|
+
onClick: handleClear,
|
|
89
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icon_cross.IconCross, {
|
|
90
|
+
width: 16,
|
|
91
|
+
height: 16
|
|
92
|
+
})
|
|
93
|
+
}) : null]
|
|
14
94
|
});
|
|
15
95
|
});
|
|
16
96
|
exports.Input = Input;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.cjs","names":["forwardRef","cn","when"],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"input.cjs","names":["forwardRef","useFieldControl","useRef","useState","useCallback","cn","when","IconCross"],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type InputHTMLAttributes,\n type MouseEvent,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { IconCross } from '@/icons/icon-cross';\nimport { cn, when } from '@/lib/cn';\nimport './input.scss';\n\nexport type InputSize = 'sm' | 'md' | 'lg';\n\n/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */\nexport interface InputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'size'\n> {\n size?: InputSize;\n recommended?: boolean;\n error?: boolean;\n /** Крестик справа. Появляется, только когда в поле есть значение. */\n clearable?: boolean;\n clearLabel?: string;\n onClear?: () => void;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | undefined,\n node: HTMLInputElement | null,\n) => {\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n\n if (ref) ref.current = node;\n};\n\nconst toInputValue = (value: InputHTMLAttributes<HTMLInputElement>['value']) =>\n value == null ? '' : String(value);\n\n/**\n * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.\n * Сеттер прототипа трекер не трогает.\n */\nconst emitInputValue = (input: HTMLInputElement, next: string) => {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n 'value',\n )?.set;\n\n setter?.call(input, next);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n};\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n type = 'text',\n size = 'md',\n recommended,\n error,\n clearable = false,\n clearLabel = 'Очистить',\n onClear,\n className,\n disabled,\n readOnly,\n value,\n defaultValue,\n onChange,\n ...props\n },\n ref,\n) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const inputRef = useRef<HTMLInputElement>(null);\n const isControlled = value !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(() =>\n toInputValue(defaultValue),\n );\n const current = isControlled ? toInputValue(value) : uncontrolledValue;\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\n const describedBy = [field?.describedBy, props['aria-describedby']]\n .filter(part => Boolean(part))\n .join(' ');\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form || isControlled) return;\n\n // reset всплывает от form вверх и до инпута не доходит\n const handleReset = () => {\n setUncontrolledValue(toInputValue(defaultValue));\n };\n\n form.addEventListener('reset', handleReset);\n return () => form.removeEventListener('reset', handleReset);\n }, [isControlled, defaultValue]);\n\n useEffect(() => {\n reportValue?.(current);\n }, [current, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (!isControlled) setUncontrolledValue(event.target.value);\n onChange?.(event);\n };\n\n const handleClearMouseDown = (event: MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n };\n\n const handleClear = () => {\n const input = inputRef.current;\n if (!input) return;\n\n emitInputValue(input, '');\n onClear?.();\n input.focus();\n };\n\n const canClear = clearable && !disabled && !readOnly && current !== '';\n\n return (\n <div className=\"input__wrap\">\n <input\n {...props}\n ref={setRef}\n type={type}\n disabled={disabled}\n readOnly={readOnly}\n {...(isControlled ? { value } : { defaultValue })}\n onChange={handleChange}\n {...(isInvalid ? { 'aria-invalid': true } : {})}\n {...(field ? { id: field.id } : {})}\n {...(field?.required\n ? { 'required': true, 'aria-required': true }\n : {})}\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n className={cn(\n 'input',\n className,\n when(size !== 'md', `input--${size}`),\n when(recommended, 'input--recommended'),\n when(isInvalid, 'input--error'),\n when(clearable && !disabled && !readOnly, 'input--clearable'),\n )}\n />\n {canClear ? (\n <button\n type=\"button\"\n className=\"input__clear\"\n aria-label={clearLabel}\n onMouseDown={handleClearMouseDown}\n onClick={handleClear}\n >\n <IconCross width={16} height={16} />\n </button>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { InputHTMLAttributes } from "react";
|
|
2
|
-
export
|
|
2
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */
|
|
4
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
5
|
+
size?: InputSize;
|
|
3
6
|
recommended?: boolean;
|
|
4
7
|
error?: boolean;
|
|
8
|
+
/** Крестик справа. Появляется, только когда в поле есть значение. */
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
clearLabel?: string;
|
|
11
|
+
onClear?: () => void;
|
|
5
12
|
}
|
|
6
13
|
export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
7
14
|
//# sourceMappingURL=input.d.cts.map
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { InputHTMLAttributes } from "react";
|
|
2
|
-
export
|
|
2
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */
|
|
4
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
5
|
+
size?: InputSize;
|
|
3
6
|
recommended?: boolean;
|
|
4
7
|
error?: boolean;
|
|
8
|
+
/** Крестик справа. Появляется, только когда в поле есть значение. */
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
clearLabel?: string;
|
|
11
|
+
onClear?: () => void;
|
|
5
12
|
}
|
|
6
13
|
export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
7
14
|
//# sourceMappingURL=input.d.ts.map
|
|
@@ -1,16 +1,96 @@
|
|
|
1
1
|
import { cn, when } from "../../lib/cn.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useFieldControl } from "../field/field-context.js";
|
|
3
|
+
import { IconCross } from "../../icons/icon-cross.js";
|
|
4
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
6
|
import "./input.scss";
|
|
5
7
|
// src/components/input/input.tsx
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
ref
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const assignRef = (ref, node) => {
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
ref(node);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (ref) ref.current = node;
|
|
14
|
+
};
|
|
15
|
+
const toInputValue = (value) => value == null ? "" : String(value);
|
|
16
|
+
/**
|
|
17
|
+
* Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.
|
|
18
|
+
* Сеттер прототипа трекер не трогает.
|
|
19
|
+
*/
|
|
20
|
+
const emitInputValue = (input, next) => {
|
|
21
|
+
(Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set)?.call(input, next);
|
|
22
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
23
|
+
};
|
|
24
|
+
const Input = forwardRef(function Input({ type = "text", size = "md", recommended, error, clearable = false, clearLabel = "Очистить", onClear, className, disabled, readOnly, value, defaultValue, onChange, ...props }, ref) {
|
|
25
|
+
const field = useFieldControl();
|
|
26
|
+
const reportValue = field?.reportValue;
|
|
27
|
+
const inputRef = useRef(null);
|
|
28
|
+
const isControlled = value !== void 0;
|
|
29
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(() => toInputValue(defaultValue));
|
|
30
|
+
const current = isControlled ? toInputValue(value) : uncontrolledValue;
|
|
31
|
+
const isInvalid = Boolean(error) || Boolean(field?.invalid);
|
|
32
|
+
const describedBy = [field?.describedBy, props["aria-describedby"]].filter((part) => Boolean(part)).join(" ");
|
|
33
|
+
const setRef = useCallback((node) => {
|
|
34
|
+
inputRef.current = node;
|
|
35
|
+
assignRef(ref, node);
|
|
36
|
+
}, [ref]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const form = inputRef.current?.form;
|
|
39
|
+
if (!form || isControlled) return;
|
|
40
|
+
const handleReset = () => {
|
|
41
|
+
setUncontrolledValue(toInputValue(defaultValue));
|
|
42
|
+
};
|
|
43
|
+
form.addEventListener("reset", handleReset);
|
|
44
|
+
return () => form.removeEventListener("reset", handleReset);
|
|
45
|
+
}, [isControlled, defaultValue]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
reportValue?.(current);
|
|
48
|
+
}, [current, reportValue]);
|
|
49
|
+
const handleChange = (event) => {
|
|
50
|
+
if (!isControlled) setUncontrolledValue(event.target.value);
|
|
51
|
+
onChange?.(event);
|
|
52
|
+
};
|
|
53
|
+
const handleClearMouseDown = (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
};
|
|
56
|
+
const handleClear = () => {
|
|
57
|
+
const input = inputRef.current;
|
|
58
|
+
if (!input) return;
|
|
59
|
+
emitInputValue(input, "");
|
|
60
|
+
onClear?.();
|
|
61
|
+
input.focus();
|
|
62
|
+
};
|
|
63
|
+
const canClear = clearable && !disabled && !readOnly && current !== "";
|
|
64
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "input__wrap",
|
|
66
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
67
|
+
...props,
|
|
68
|
+
ref: setRef,
|
|
69
|
+
type,
|
|
70
|
+
disabled,
|
|
71
|
+
readOnly,
|
|
72
|
+
...isControlled ? { value } : { defaultValue },
|
|
73
|
+
onChange: handleChange,
|
|
74
|
+
...isInvalid ? { "aria-invalid": true } : {},
|
|
75
|
+
...field ? { id: field.id } : {},
|
|
76
|
+
...field?.required ? {
|
|
77
|
+
"required": true,
|
|
78
|
+
"aria-required": true
|
|
79
|
+
} : {},
|
|
80
|
+
...describedBy ? { "aria-describedby": describedBy } : {},
|
|
81
|
+
...field?.errorId ? { "aria-errormessage": field.errorId } : {},
|
|
82
|
+
className: cn("input", className, when(size !== "md", `input--${size}`), when(recommended, "input--recommended"), when(isInvalid, "input--error"), when(clearable && !disabled && !readOnly, "input--clearable"))
|
|
83
|
+
}), canClear ? /* @__PURE__ */ jsx("button", {
|
|
84
|
+
type: "button",
|
|
85
|
+
className: "input__clear",
|
|
86
|
+
"aria-label": clearLabel,
|
|
87
|
+
onMouseDown: handleClearMouseDown,
|
|
88
|
+
onClick: handleClear,
|
|
89
|
+
children: /* @__PURE__ */ jsx(IconCross, {
|
|
90
|
+
width: 16,
|
|
91
|
+
height: 16
|
|
92
|
+
})
|
|
93
|
+
}) : null]
|
|
14
94
|
});
|
|
15
95
|
});
|
|
16
96
|
export { Input };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type InputHTMLAttributes,\n type MouseEvent,\n type Ref,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { IconCross } from '@/icons/icon-cross';\nimport { cn, when } from '@/lib/cn';\nimport './input.scss';\n\nexport type InputSize = 'sm' | 'md' | 'lg';\n\n/** Нативный `size` (ширина в символах) занят размером контрола: высоты как у `Button`. */\nexport interface InputProps extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n 'size'\n> {\n size?: InputSize;\n recommended?: boolean;\n error?: boolean;\n /** Крестик справа. Появляется, только когда в поле есть значение. */\n clearable?: boolean;\n clearLabel?: string;\n onClear?: () => void;\n}\n\nconst assignRef = (\n ref: Ref<HTMLInputElement> | undefined,\n node: HTMLInputElement | null,\n) => {\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n\n if (ref) ref.current = node;\n};\n\nconst toInputValue = (value: InputHTMLAttributes<HTMLInputElement>['value']) =>\n value == null ? '' : String(value);\n\n/**\n * Прямая запись `input.value` обновляет value-tracker React, и onChange не стреляет.\n * Сеттер прототипа трекер не трогает.\n */\nconst emitInputValue = (input: HTMLInputElement, next: string) => {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLInputElement.prototype,\n 'value',\n )?.set;\n\n setter?.call(input, next);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n};\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n type = 'text',\n size = 'md',\n recommended,\n error,\n clearable = false,\n clearLabel = 'Очистить',\n onClear,\n className,\n disabled,\n readOnly,\n value,\n defaultValue,\n onChange,\n ...props\n },\n ref,\n) {\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const inputRef = useRef<HTMLInputElement>(null);\n const isControlled = value !== undefined;\n const [uncontrolledValue, setUncontrolledValue] = useState(() =>\n toInputValue(defaultValue),\n );\n const current = isControlled ? toInputValue(value) : uncontrolledValue;\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\n const describedBy = [field?.describedBy, props['aria-describedby']]\n .filter(part => Boolean(part))\n .join(' ');\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n\n useEffect(() => {\n const form = inputRef.current?.form;\n if (!form || isControlled) return;\n\n // reset всплывает от form вверх и до инпута не доходит\n const handleReset = () => {\n setUncontrolledValue(toInputValue(defaultValue));\n };\n\n form.addEventListener('reset', handleReset);\n return () => form.removeEventListener('reset', handleReset);\n }, [isControlled, defaultValue]);\n\n useEffect(() => {\n reportValue?.(current);\n }, [current, reportValue]);\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (!isControlled) setUncontrolledValue(event.target.value);\n onChange?.(event);\n };\n\n const handleClearMouseDown = (event: MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n };\n\n const handleClear = () => {\n const input = inputRef.current;\n if (!input) return;\n\n emitInputValue(input, '');\n onClear?.();\n input.focus();\n };\n\n const canClear = clearable && !disabled && !readOnly && current !== '';\n\n return (\n <div className=\"input__wrap\">\n <input\n {...props}\n ref={setRef}\n type={type}\n disabled={disabled}\n readOnly={readOnly}\n {...(isControlled ? { value } : { defaultValue })}\n onChange={handleChange}\n {...(isInvalid ? { 'aria-invalid': true } : {})}\n {...(field ? { id: field.id } : {})}\n {...(field?.required\n ? { 'required': true, 'aria-required': true }\n : {})}\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n className={cn(\n 'input',\n className,\n when(size !== 'md', `input--${size}`),\n when(recommended, 'input--recommended'),\n when(isInvalid, 'input--error'),\n when(clearable && !disabled && !readOnly, 'input--clearable'),\n )}\n />\n {canClear ? (\n <button\n type=\"button\"\n className=\"input__clear\"\n aria-label={clearLabel}\n onMouseDown={handleClearMouseDown}\n onClick={handleClear}\n >\n <IconCross width={16} height={16} />\n </button>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
.input {
|
|
2
|
+
// Размер контрола. Высоты совпадают с Button: sm 30px, md 40px, lg 52px.
|
|
3
|
+
--input-padding-y: 9px;
|
|
4
|
+
--input-padding-x: 14px;
|
|
5
|
+
--input-height: 40px;
|
|
2
6
|
box-sizing: border-box;
|
|
3
7
|
display: flex;
|
|
4
8
|
align-items: center;
|
|
5
9
|
width: 100%;
|
|
6
|
-
padding:
|
|
7
|
-
font-size:
|
|
8
|
-
line-height:
|
|
10
|
+
padding: var(--input-padding-y) var(--input-padding-x);
|
|
11
|
+
font-size: var(--text-sm);
|
|
12
|
+
line-height: var(--text-sm--line-height);
|
|
9
13
|
background-color: transparent;
|
|
10
|
-
color: var(--color-
|
|
14
|
+
color: var(--color-foreground);
|
|
11
15
|
border-radius: var(--radius-sm);
|
|
12
16
|
appearance: none;
|
|
13
17
|
font-family: var(--font-sans);
|
|
@@ -18,11 +22,19 @@
|
|
|
18
22
|
border-color 0.3s,
|
|
19
23
|
box-shadow 0.2s;
|
|
20
24
|
|
|
25
|
+
// Фон автозаполнения Chrome перебивается только внутренней тенью.
|
|
21
26
|
&:-webkit-autofill,
|
|
22
27
|
&:-webkit-autofill:hover,
|
|
23
|
-
&:-webkit-autofill:focus,
|
|
24
28
|
&:-webkit-autofill:active {
|
|
25
|
-
-webkit-box-shadow: 0 0 0 30px
|
|
29
|
+
-webkit-box-shadow: 0 0 0 30px var(--color-background) inset;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// :-webkit-autofill:focus специфичнее :focus и затирает его box-shadow,
|
|
33
|
+
// поэтому обводка фокуса повторяется здесь второй тенью.
|
|
34
|
+
&:-webkit-autofill:focus {
|
|
35
|
+
-webkit-box-shadow:
|
|
36
|
+
0 0 0 30px var(--color-background) inset,
|
|
37
|
+
0 0 0 var(--focus-ring-width) var(--focus-ring-color);
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
&[type='number']::-webkit-outer-spin-button,
|
|
@@ -36,7 +48,7 @@
|
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
&[type='date'] {
|
|
39
|
-
height:
|
|
51
|
+
height: var(--input-height);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
&::-webkit-date-and-time-value {
|
|
@@ -44,21 +56,72 @@
|
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
&:focus {
|
|
47
|
-
border-color: var(--color-
|
|
48
|
-
box-shadow: 0 0 0
|
|
59
|
+
border-color: var(--color-ring);
|
|
60
|
+
box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring-color);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
&:disabled {
|
|
52
|
-
background-color: var(--color-
|
|
53
|
-
color: var(--color-
|
|
54
|
-
fill: var(--color-
|
|
64
|
+
background-color: var(--color-muted);
|
|
65
|
+
color: var(--color-muted-foreground);
|
|
66
|
+
fill: var(--color-muted-foreground);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&--sm {
|
|
70
|
+
--input-padding-y: 4px;
|
|
71
|
+
--input-padding-x: 12px;
|
|
72
|
+
--input-height: 30px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&--lg {
|
|
76
|
+
--input-padding-y: var(--space-3);
|
|
77
|
+
--input-padding-x: 24px;
|
|
78
|
+
--input-height: 52px;
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
&--recommended {
|
|
58
|
-
background-color:
|
|
82
|
+
background-color: var(--color-recommended);
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
&--error {
|
|
62
|
-
border-color: color-mix(in srgb, var(--color-
|
|
86
|
+
border-color: color-mix(in srgb, var(--color-destructive) 50%, transparent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&--clearable {
|
|
90
|
+
padding-right: 36px;
|
|
91
|
+
|
|
92
|
+
&[type='search']::-webkit-search-cancel-button {
|
|
93
|
+
-webkit-appearance: none;
|
|
94
|
+
appearance: none;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.input__wrap {
|
|
100
|
+
position: relative;
|
|
101
|
+
width: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.input__clear {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: 0;
|
|
107
|
+
right: 4px;
|
|
108
|
+
bottom: 0;
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
width: 28px;
|
|
113
|
+
margin: 0;
|
|
114
|
+
padding: 0;
|
|
115
|
+
border: 0;
|
|
116
|
+
background: none;
|
|
117
|
+
color: var(--color-muted-foreground);
|
|
118
|
+
|
|
119
|
+
&:hover {
|
|
120
|
+
color: var(--color-foreground);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:focus-visible {
|
|
124
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
125
|
+
border-radius: var(--radius-sm);
|
|
63
126
|
}
|
|
64
127
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { RadioInput, RadioInputProps } from "./radio.cjs";
|
|
2
|
-
export { RadioInput, type RadioInputProps };
|
|
1
|
+
import { RadioGroup, RadioGroupProps, RadioInput, RadioInputProps } from "./radio.cjs";
|
|
2
|
+
export { RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { RadioInput, RadioInputProps } from "./radio.js";
|
|
2
|
-
export { RadioInput, type RadioInputProps };
|
|
1
|
+
import { RadioGroup, RadioGroupProps, RadioInput, RadioInputProps } from "./radio.js";
|
|
2
|
+
export { RadioGroup, type RadioGroupProps, RadioInput, type RadioInputProps };
|
|
@@ -1,27 +1,69 @@
|
|
|
1
1
|
const require_cn = require("../../lib/cn.cjs");
|
|
2
|
+
const require_field_context = require("../field/field-context.cjs");
|
|
2
3
|
let react = require("react");
|
|
3
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
5
|
require("./radio.scss");
|
|
5
6
|
// src/components/radio/radio.tsx
|
|
6
|
-
const
|
|
7
|
+
const RadioGroupStateContext = (0, react.createContext)(null);
|
|
8
|
+
const RadioGroup = ({ label, name, required: requiredProp = false, children, className }) => {
|
|
9
|
+
const field = require_field_context.useFieldControl();
|
|
10
|
+
const required = requiredProp || Boolean(field?.required);
|
|
11
|
+
const labelledBy = !label && field?.labelId ? field.labelId : void 0;
|
|
12
|
+
const reportValue = field?.reportValue;
|
|
13
|
+
const group = (0, react.useMemo)(() => ({
|
|
14
|
+
name,
|
|
15
|
+
required,
|
|
16
|
+
reportValue
|
|
17
|
+
}), [
|
|
18
|
+
name,
|
|
19
|
+
required,
|
|
20
|
+
reportValue
|
|
21
|
+
]);
|
|
22
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
23
|
+
className: require_cn.cn("radio-group", className),
|
|
24
|
+
role: "radiogroup",
|
|
25
|
+
...labelledBy ? { "aria-labelledby": labelledBy } : {},
|
|
26
|
+
...field?.invalid ? { "aria-invalid": true } : {},
|
|
27
|
+
...required ? { "aria-required": true } : {},
|
|
28
|
+
...field?.describedBy ? { "aria-describedby": field.describedBy } : {},
|
|
29
|
+
...field?.errorId ? { "aria-errormessage": field.errorId } : {},
|
|
30
|
+
children: [label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", {
|
|
31
|
+
className: "radio-group__legend",
|
|
32
|
+
children: label
|
|
33
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RadioGroupStateContext.Provider, {
|
|
34
|
+
value: group,
|
|
35
|
+
children
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const RadioInput = (0, react.forwardRef)(function RadioInput({ id, className, label, error, disabled, name, onChange, ...props }, ref) {
|
|
40
|
+
const group = (0, react.useContext)(RadioGroupStateContext);
|
|
7
41
|
const autoId = (0, react.useId)();
|
|
8
42
|
const inputId = id ?? autoId;
|
|
43
|
+
const handleChange = (event) => {
|
|
44
|
+
if (event.target.checked) group?.reportValue?.(event.target.value);
|
|
45
|
+
onChange?.(event);
|
|
46
|
+
};
|
|
9
47
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10
48
|
className: require_cn.cn("radio", className, require_cn.when(error, "radio--error")),
|
|
11
49
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
50
|
+
...props,
|
|
12
51
|
ref,
|
|
13
52
|
type: "radio",
|
|
14
53
|
id: inputId,
|
|
54
|
+
name: group?.name ?? name,
|
|
15
55
|
disabled,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
56
|
+
...group?.required ? { required: true } : {},
|
|
57
|
+
onChange: handleChange,
|
|
58
|
+
className: "radio__input"
|
|
59
|
+
}), label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
19
60
|
className: "radio__label",
|
|
20
61
|
htmlFor: inputId,
|
|
21
|
-
children:
|
|
62
|
+
children: label
|
|
22
63
|
}) : null]
|
|
23
64
|
});
|
|
24
65
|
});
|
|
66
|
+
exports.RadioGroup = RadioGroup;
|
|
25
67
|
exports.RadioInput = RadioInput;
|
|
26
68
|
|
|
27
69
|
//# sourceMappingURL=radio.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio.cjs","names":["
|
|
1
|
+
{"version":3,"file":"radio.cjs","names":["createContext","useFieldControl","useMemo","cn","forwardRef","useContext","useId","when"],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import {\n createContext,\n forwardRef,\n useContext,\n useId,\n useMemo,\n type ChangeEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { cn, when } from '@/lib/cn';\nimport './radio.scss';\n\ninterface RadioGroupState {\n name: string;\n required: boolean;\n reportValue?: (value: string) => void;\n}\n\nconst RadioGroupStateContext = createContext<RadioGroupState | null>(null);\n\nexport interface RadioGroupProps {\n /** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */\n label?: ReactNode;\n name: string;\n /** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */\n required?: boolean;\n children: ReactNode;\n className?: string;\n}\n\nexport const RadioGroup = ({\n label,\n name,\n required: requiredProp = false,\n children,\n className,\n}: RadioGroupProps) => {\n const field = useFieldControl();\n const required = requiredProp || Boolean(field?.required);\n const labelledBy = !label && field?.labelId ? field.labelId : undefined;\n // Обязательность держат пункты: нативный required на них не даёт отправить\n // форму, а его invalid Field ловит и показывает свою ошибку. Выбор отдаём\n // в reportValue — иначе показанная ошибка так и висела бы после выбора.\n const reportValue = field?.reportValue;\n const group = useMemo<RadioGroupState>(\n () => ({ name, required, reportValue }),\n [name, required, reportValue],\n );\n\n return (\n <fieldset\n className={cn('radio-group', className)}\n role=\"radiogroup\"\n {...(labelledBy ? { 'aria-labelledby': labelledBy } : {})}\n {...(field?.invalid ? { 'aria-invalid': true } : {})}\n {...(required ? { 'aria-required': true } : {})}\n {...(field?.describedBy ? { 'aria-describedby': field.describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n >\n {label ? <legend className=\"radio-group__legend\">{label}</legend> : null}\n <RadioGroupStateContext.Provider value={group}>\n {children}\n </RadioGroupStateContext.Provider>\n </fieldset>\n );\n};\n\nexport interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n error?: boolean;\n}\n\nexport const RadioInput = forwardRef<HTMLInputElement, RadioInputProps>(\n function RadioInput(\n { id, className, label, error, disabled, name, onChange, ...props },\n ref,\n ) {\n const group = useContext(RadioGroupStateContext);\n const autoId = useId();\n const inputId = id ?? autoId;\n\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (event.target.checked) group?.reportValue?.(event.target.value);\n onChange?.(event);\n };\n\n return (\n <div className={cn('radio', className, when(error, 'radio--error'))}>\n <input\n {...props}\n ref={ref}\n type=\"radio\"\n id={inputId}\n // Внутри группы имя группы побеждает, иначе её можно разрезать name на пункте.\n name={group?.name ?? name}\n disabled={disabled}\n // required на любом пункте делает обязательной всю группу с этим name.\n {...(group?.required ? { required: true } : {})}\n onChange={handleChange}\n className=\"radio__input\"\n />\n {label ? (\n <label className=\"radio__label\" htmlFor={inputId}>\n {label}\n </label>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;;AAqBA,MAAM,0BAAA,GAAyBA,MAAAA,cAAAA,CAAsC,IAAI;AAYzE,MAAa,cAAc,EACzB,OACA,MACA,UAAU,eAAe,OACzB,UACA,gBACqB;CACrB,MAAM,QAAQC,sBAAAA,gBAAgB;CAC9B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,QAAQ;CACxD,MAAM,aAAa,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,KAAA;CAI9D,MAAM,cAAc,OAAO;CAC3B,MAAM,SAAA,GAAQC,MAAAA,QAAAA,QACL;EAAE;EAAM;EAAU;CAAY,IACrC;EAAC;EAAM;EAAU;CAAW,CAC9B;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,YAAD;EACE,WAAWC,WAAAA,GAAG,eAAe,SAAS;EACtC,MAAK;EACL,GAAK,aAAa,EAAE,mBAAmB,WAAW,IAAI,CAAC;EACvD,GAAK,OAAO,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAC;EAClD,GAAK,WAAW,EAAE,iBAAiB,KAAK,IAAI,CAAC;EAC7C,GAAK,OAAO,cAAc,EAAE,oBAAoB,MAAM,YAAY,IAAI,CAAC;EACvE,GAAK,OAAO,UAAU,EAAE,qBAAqB,MAAM,QAAQ,IAAI,CAAC;EAPlE,UAAA,CASG,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;GAAQ,WAAU;GAAuB,UAAA;EAAc,CAAA,IAAI,MACpE,iBAAA,GAAA,kBAAA,IAAA,CAAC,uBAAuB,UAAxB;GAAiC,OAAO;GACrC;EAC8B,CAAA,CACzB;;AAEd;AAOA,MAAa,cAAA,GAAaC,MAAAA,WAAAA,CACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,MAAM,UAAU,GAAG,SAC5D,KACA;CACA,MAAM,SAAA,GAAQC,MAAAA,WAAAA,CAAW,sBAAsB;CAC/C,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,MAAM,OAAO,SAAS,OAAO,cAAc,MAAM,OAAO,KAAK;EACjE,WAAW,KAAK;CAClB;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAWH,WAAAA,GAAG,SAAS,WAAWI,WAAAA,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GACE,GAAI;GACC;GACL,MAAK;GACL,IAAI;GAEJ,MAAM,OAAO,QAAQ;GACX;GAEV,GAAK,OAAO,WAAW,EAAE,UAAU,KAAK,IAAI,CAAC;GAC7C,UAAU;GACV,WAAU;EACX,CAAA,GACA,QACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { InputHTMLAttributes } from "react";
|
|
1
|
+
import { InputHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface RadioGroupProps {
|
|
3
|
+
/** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */
|
|
4
|
+
label?: ReactNode;
|
|
5
|
+
name: string;
|
|
6
|
+
/** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */
|
|
7
|
+
required?: boolean;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const RadioGroup: ({ label, name, required: requiredProp, children, className }: RadioGroupProps) => import("react").JSX.Element;
|
|
2
12
|
export interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
3
|
-
|
|
13
|
+
label?: ReactNode;
|
|
4
14
|
error?: boolean;
|
|
5
15
|
}
|
|
6
16
|
export declare const RadioInput: import("react").ForwardRefExoticComponent<RadioInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { InputHTMLAttributes } from "react";
|
|
1
|
+
import { InputHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface RadioGroupProps {
|
|
3
|
+
/** Легенда группы. Внутри `Field` не нужна: именем станет лейбл поля. */
|
|
4
|
+
label?: ReactNode;
|
|
5
|
+
name: string;
|
|
6
|
+
/** Внутри `Field` его `required` и так включает группу, здесь — для группы отдельно. */
|
|
7
|
+
required?: boolean;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const RadioGroup: ({ label, name, required: requiredProp, children, className }: RadioGroupProps) => import("react").JSX.Element;
|
|
2
12
|
export interface RadioInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
3
|
-
|
|
13
|
+
label?: ReactNode;
|
|
4
14
|
error?: boolean;
|
|
5
15
|
}
|
|
6
16
|
export declare const RadioInput: import("react").ForwardRefExoticComponent<RadioInputProps & import("react").RefAttributes<HTMLInputElement>>;
|