@de-kit/st-ui 0.1.5 → 0.1.6
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
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
import { cn, when } from "../../lib/cn.js";
|
|
2
|
-
import {
|
|
2
|
+
import { useFieldControl } from "../field/field-context.js";
|
|
3
|
+
import { createContext, forwardRef, useContext, useId, useMemo } from "react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import "./radio.scss";
|
|
5
6
|
// src/components/radio/radio.tsx
|
|
6
|
-
const
|
|
7
|
+
const RadioGroupStateContext = createContext(null);
|
|
8
|
+
const RadioGroup = ({ label, name, required: requiredProp = false, children, className }) => {
|
|
9
|
+
const field = 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 = useMemo(() => ({
|
|
14
|
+
name,
|
|
15
|
+
required,
|
|
16
|
+
reportValue
|
|
17
|
+
}), [
|
|
18
|
+
name,
|
|
19
|
+
required,
|
|
20
|
+
reportValue
|
|
21
|
+
]);
|
|
22
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
23
|
+
className: 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__ */ jsx("legend", {
|
|
31
|
+
className: "radio-group__legend",
|
|
32
|
+
children: label
|
|
33
|
+
}) : null, /* @__PURE__ */ jsx(RadioGroupStateContext.Provider, {
|
|
34
|
+
value: group,
|
|
35
|
+
children
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const RadioInput = forwardRef(function RadioInput({ id, className, label, error, disabled, name, onChange, ...props }, ref) {
|
|
40
|
+
const group = useContext(RadioGroupStateContext);
|
|
7
41
|
const autoId = 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__ */ jsxs("div", {
|
|
10
48
|
className: cn("radio", className, when(error, "radio--error")),
|
|
11
49
|
children: [/* @__PURE__ */ 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__ */ jsx("label", {
|
|
19
60
|
className: "radio__label",
|
|
20
61
|
htmlFor: inputId,
|
|
21
|
-
children:
|
|
62
|
+
children: label
|
|
22
63
|
}) : null]
|
|
23
64
|
});
|
|
24
65
|
});
|
|
25
|
-
export { RadioInput };
|
|
66
|
+
export { RadioGroup, RadioInput };
|
|
26
67
|
|
|
27
68
|
//# sourceMappingURL=radio.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio.js","names":[],"sources":["../../../src/components/radio/radio.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"radio.js","names":[],"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,yBAAyB,cAAsC,IAAI;AAYzE,MAAa,cAAc,EACzB,OACA,MACA,UAAU,eAAe,OACzB,UACA,gBACqB;CACrB,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,QAAQ;CACxD,MAAM,aAAa,CAAC,SAAS,OAAO,UAAU,MAAM,UAAU,KAAA;CAI9D,MAAM,cAAc,OAAO;CAC3B,MAAM,QAAQ,eACL;EAAE;EAAM;EAAU;CAAY,IACrC;EAAC;EAAM;EAAU;CAAW,CAC9B;CAEA,OACE,qBAAC,YAAD;EACE,WAAW,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,oBAAC,UAAD;GAAQ,WAAU;GAAuB,UAAA;EAAc,CAAA,IAAI,MACpE,oBAAC,uBAAuB,UAAxB;GAAiC,OAAO;GACrC;EAC8B,CAAA,CACzB;;AAEd;AAOA,MAAa,aAAa,WACxB,SAAS,WACP,EAAE,IAAI,WAAW,OAAO,OAAO,UAAU,MAAM,UAAU,GAAG,SAC5D,KACA;CACA,MAAM,QAAQ,WAAW,sBAAsB;CAC/C,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM;CAEtB,MAAM,gBAAgB,UAAyC;EAC7D,IAAI,MAAM,OAAO,SAAS,OAAO,cAAc,MAAM,OAAO,KAAK;EACjE,WAAW,KAAK;CAClB;CAEA,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,SAAS,WAAW,KAAK,OAAO,cAAc,CAAC;EAAlE,UAAA,CACE,oBAAC,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,oBAAC,SAAD;GAAO,WAAU;GAAe,SAAS;GACtC,UAAA;EACI,CAAA,IACL,IACD;;AAET,CACF"}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
.radio__label {
|
|
2
|
-
font-size:
|
|
3
|
-
line-height:
|
|
2
|
+
font-size: var(--text-sm);
|
|
3
|
+
line-height: var(--text-sm--line-height);
|
|
4
4
|
display: inline-block;
|
|
5
5
|
vertical-align: top;
|
|
6
|
-
padding-left:
|
|
6
|
+
padding-left: var(--space-1);
|
|
7
7
|
user-select: none;
|
|
8
8
|
cursor: pointer;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.radio__input {
|
|
12
|
-
--active:
|
|
13
|
-
--active-inner:
|
|
14
|
-
--border:
|
|
15
|
-
--border-hover:
|
|
16
|
-
--background:
|
|
17
|
-
--disabled:
|
|
18
|
-
--disabled-inner:
|
|
19
|
-
--disabled-after-inner:
|
|
12
|
+
--active: var(--color-primary);
|
|
13
|
+
--active-inner: var(--color-primary-foreground);
|
|
14
|
+
--border: var(--color-muted);
|
|
15
|
+
--border-hover: var(--color-foreground);
|
|
16
|
+
--background: var(--color-background);
|
|
17
|
+
--disabled: var(--color-muted);
|
|
18
|
+
--disabled-inner: var(--color-muted);
|
|
19
|
+
--disabled-after-inner: var(--color-muted-foreground);
|
|
20
20
|
box-sizing: border-box;
|
|
21
21
|
appearance: none;
|
|
22
|
-
width:
|
|
23
|
-
height:
|
|
22
|
+
width: 20px;
|
|
23
|
+
height: 20px;
|
|
24
24
|
outline: none;
|
|
25
25
|
display: inline-block;
|
|
26
26
|
vertical-align: top;
|
|
@@ -75,6 +75,15 @@
|
|
|
75
75
|
border: 1px solid var(--border-hover);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
&:focus:not(:focus-visible) {
|
|
79
|
+
outline: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&:focus-visible {
|
|
83
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
84
|
+
outline-offset: var(--focus-ring-offset);
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
&:disabled {
|
|
79
88
|
--b: var(--disabled);
|
|
80
89
|
cursor: not-allowed;
|
|
@@ -90,7 +99,7 @@
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
& + .radio__label {
|
|
93
|
-
color: var(--color-
|
|
102
|
+
color: var(--color-muted-foreground);
|
|
94
103
|
cursor: not-allowed;
|
|
95
104
|
}
|
|
96
105
|
}
|
|
@@ -103,7 +112,33 @@
|
|
|
103
112
|
|
|
104
113
|
&--error {
|
|
105
114
|
.radio__input {
|
|
106
|
-
border-color: color-mix(
|
|
115
|
+
border-color: color-mix(
|
|
116
|
+
in srgb,
|
|
117
|
+
var(--color-destructive) 50%,
|
|
118
|
+
transparent
|
|
119
|
+
);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
}
|
|
123
|
+
|
|
124
|
+
.radio-group {
|
|
125
|
+
margin: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
border: 0;
|
|
128
|
+
min-inline-size: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.radio-group__legend {
|
|
132
|
+
margin: 0 0 var(--space-3);
|
|
133
|
+
color: var(--color-foreground);
|
|
134
|
+
font-size: var(--text-lg);
|
|
135
|
+
font-weight: 900;
|
|
136
|
+
line-height: var(--text-lg--line-height);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (prefers-reduced-motion: reduce) {
|
|
140
|
+
.radio__input,
|
|
141
|
+
.radio__input::after {
|
|
142
|
+
transition: none;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_cn = require("../../lib/cn.cjs");
|
|
2
|
+
let react = require("react");
|
|
2
3
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
3
4
|
require("./spinner.scss");
|
|
4
5
|
// src/components/spinner/spinner.tsx
|
|
@@ -6,6 +7,15 @@ const spinnerPosition = {
|
|
|
6
7
|
top: "top",
|
|
7
8
|
center: "center"
|
|
8
9
|
};
|
|
10
|
+
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
|
|
11
|
+
const subscribeReducedMotion = (onChange) => {
|
|
12
|
+
if (typeof window.matchMedia !== "function") return () => {};
|
|
13
|
+
const media = window.matchMedia(reducedMotionQuery);
|
|
14
|
+
media.addEventListener("change", onChange);
|
|
15
|
+
return () => media.removeEventListener("change", onChange);
|
|
16
|
+
};
|
|
17
|
+
const prefersReducedMotion = () => typeof window.matchMedia === "function" && window.matchMedia(reducedMotionQuery).matches;
|
|
18
|
+
const prefersReducedMotionServer = () => false;
|
|
9
19
|
const dotDelays = [
|
|
10
20
|
"0s",
|
|
11
21
|
"0.125s",
|
|
@@ -16,12 +26,12 @@ const dotDelays = [
|
|
|
16
26
|
"0.75s",
|
|
17
27
|
"0.875s"
|
|
18
28
|
];
|
|
19
|
-
const Dot = ({ begin, rotate }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
29
|
+
const Dot = ({ begin, rotate, reduced }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
20
30
|
transform: `rotate(${rotate} 16 16)`,
|
|
21
31
|
cx: "16",
|
|
22
32
|
cy: "3",
|
|
23
|
-
r:
|
|
24
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("animate", {
|
|
33
|
+
r: reduced ? 3 : 0,
|
|
34
|
+
children: reduced ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("animate", {
|
|
25
35
|
attributeName: "r",
|
|
26
36
|
values: "0;3;0;0",
|
|
27
37
|
dur: "1s",
|
|
@@ -34,21 +44,33 @@ const Dot = ({ begin, rotate }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("c
|
|
|
34
44
|
const svgProps = {
|
|
35
45
|
"xmlns": "http://www.w3.org/2000/svg",
|
|
36
46
|
"viewBox": "0 0 32 32",
|
|
37
|
-
"
|
|
38
|
-
"height": 64,
|
|
39
|
-
"fill": "#ecebeb",
|
|
47
|
+
"fill": "var(--color-muted)",
|
|
40
48
|
"aria-hidden": true
|
|
41
49
|
};
|
|
42
|
-
const Spinner = ({ className, position = spinnerPosition.center
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const Spinner = ({ className, position = spinnerPosition.center, size = 64, label = "Загрузка", ...props }) => {
|
|
51
|
+
const labelId = (0, react.useId)();
|
|
52
|
+
const reduced = (0, react.useSyncExternalStore)(subscribeReducedMotion, prefersReducedMotion, prefersReducedMotionServer);
|
|
53
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
54
|
+
...props,
|
|
55
|
+
role: "status",
|
|
56
|
+
"aria-labelledby": labelId,
|
|
57
|
+
className: require_cn.cn("spinner", className, require_cn.when(position === spinnerPosition.top, "spinner--top")),
|
|
58
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
59
|
+
id: labelId,
|
|
60
|
+
className: "spinner__label",
|
|
61
|
+
children: label
|
|
62
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
63
|
+
...svgProps,
|
|
64
|
+
width: size,
|
|
65
|
+
height: size,
|
|
66
|
+
children: dotDelays.map((begin, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dot, {
|
|
67
|
+
begin,
|
|
68
|
+
rotate: index * 45,
|
|
69
|
+
reduced
|
|
70
|
+
}, begin))
|
|
71
|
+
})]
|
|
72
|
+
});
|
|
73
|
+
};
|
|
52
74
|
exports.Spinner = Spinner;
|
|
53
75
|
|
|
54
76
|
//# sourceMappingURL=spinner.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spinner.cjs","names":["cn","when"],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import type
|
|
1
|
+
{"version":3,"file":"spinner.cjs","names":["useId","useSyncExternalStore","cn","when"],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\n useId,\n useSyncExternalStore,\n type FC,\n type HTMLAttributes,\n type SVGProps,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './spinner.scss';\n\nconst spinnerPosition = {\n top: 'top',\n center: 'center',\n} as const;\n\nexport type SpinnerPosition =\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\n\nexport interface SpinnerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'size' | 'position'\n> {\n size?: number;\n position?: SpinnerPosition;\n /** Текст для скринридера. Виден только ему. */\n label?: string;\n}\n\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\n\nconst subscribeReducedMotion = (onChange: () => void) => {\n if (typeof window.matchMedia !== 'function') return () => {};\n\n const media = window.matchMedia(reducedMotionQuery);\n media.addEventListener('change', onChange);\n return () => media.removeEventListener('change', onChange);\n};\n\nconst prefersReducedMotion = () =>\n typeof window.matchMedia === 'function' &&\n window.matchMedia(reducedMotionQuery).matches;\n\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\nconst prefersReducedMotionServer = () => false;\n\nconst dotDelays = [\n '0s',\n '0.125s',\n '0.25s',\n '0.375s',\n '0.5s',\n '0.625s',\n '0.75s',\n '0.875s',\n] as const;\n\nconst Dot = ({\n begin,\n rotate,\n reduced,\n}: {\n begin: string;\n rotate: number;\n reduced: boolean;\n}) => (\n <circle\n transform={`rotate(${rotate} 16 16)`}\n cx=\"16\"\n cy=\"3\"\n r={reduced ? 3 : 0}\n >\n {reduced ? null : (\n <animate\n attributeName=\"r\"\n values=\"0;3;0;0\"\n dur=\"1s\"\n repeatCount=\"indefinite\"\n begin={begin}\n keySplines=\"0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8\"\n calcMode=\"spline\"\n />\n )}\n </circle>\n);\n\nconst svgProps: SVGProps<SVGSVGElement> = {\n 'xmlns': 'http://www.w3.org/2000/svg',\n 'viewBox': '0 0 32 32',\n 'fill': 'var(--color-muted)',\n 'aria-hidden': true,\n};\n\nexport const Spinner: FC<SpinnerProps> = ({\n className,\n position = spinnerPosition.center,\n size = 64,\n label = 'Загрузка',\n ...props\n}) => {\n const labelId = useId();\n const reduced = useSyncExternalStore(\n subscribeReducedMotion,\n prefersReducedMotion,\n prefersReducedMotionServer,\n );\n\n return (\n <div\n {...props}\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\n role=\"status\"\n aria-labelledby={labelId}\n className={cn(\n 'spinner',\n className,\n when(position === spinnerPosition.top, 'spinner--top'),\n )}\n >\n <span id={labelId} className=\"spinner__label\">\n {label}\n </span>\n <svg {...svgProps} width={size} height={size}>\n {dotDelays.map((begin, index) => (\n <Dot\n key={begin}\n begin={begin}\n rotate={index * 45}\n reduced={reduced}\n />\n ))}\n </svg>\n </div>\n );\n};\n"],"mappings":";;;;;AAWA,MAAM,kBAAkB;CACtB,KAAK;CACL,QAAQ;AACV;AAeA,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B,aAAyB;CACvD,IAAI,OAAO,OAAO,eAAe,YAAY,aAAa,CAAC;CAE3D,MAAM,QAAQ,OAAO,WAAW,kBAAkB;CAClD,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa,MAAM,oBAAoB,UAAU,QAAQ;AAC3D;AAEA,MAAM,6BACJ,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kBAAkB,CAAC,CAAC;AAGxC,MAAM,mCAAmC;AAEzC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,OAAO,EACX,OACA,QACA,cAMA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;CACE,WAAW,UAAU,OAAO;CAC5B,IAAG;CACH,IAAG;CACH,GAAG,UAAU,IAAI;CAEhB,UAAA,UAAU,OACT,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;EACE,eAAc;EACd,QAAO;EACP,KAAI;EACJ,aAAY;EACL;EACP,YAAW;EACX,UAAS;CACV,CAAA;AAEG,CAAA;AAGV,MAAM,WAAoC;CACxC,SAAS;CACT,WAAW;CACX,QAAQ;CACR,eAAe;AACjB;AAEA,MAAa,WAA6B,EACxC,WACA,WAAW,gBAAgB,QAC3B,OAAO,IACP,QAAQ,YACR,GAAG,YACC;CACJ,MAAM,WAAA,GAAUA,MAAAA,MAAAA,CAAM;CACtB,MAAM,WAAA,GAAUC,MAAAA,qBAAAA,CACd,wBACA,sBACA,0BACF;CAEA,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EACE,GAAI;EAEJ,MAAK;EACL,mBAAiB;EACjB,WAAWC,WAAAA,GACT,WACA,WACAC,WAAAA,KAAK,aAAa,gBAAgB,KAAK,cAAc,CACvD;EATF,UAAA,CAWE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;GAAM,IAAI;GAAS,WAAU;GAC1B,UAAA;EACG,CAAA,GACN,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;GAAK,GAAI;GAAU,OAAO;GAAM,QAAQ;GACrC,UAAA,UAAU,KAAK,OAAO,UACrB,iBAAA,GAAA,kBAAA,IAAA,CAAC,KAAD;IAES;IACP,QAAQ,QAAQ;IACP;GACV,GAJM,KAIN,CACF;EACE,CAAA,CACF;;AAET"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
1
|
+
import { FC, HTMLAttributes } from "react";
|
|
2
2
|
declare const spinnerPosition: {
|
|
3
3
|
readonly top: "top";
|
|
4
4
|
readonly center: "center";
|
|
5
5
|
};
|
|
6
6
|
export type SpinnerPosition = (typeof spinnerPosition)[keyof typeof spinnerPosition];
|
|
7
|
-
export interface SpinnerProps {
|
|
8
|
-
|
|
7
|
+
export interface SpinnerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'size' | 'position'> {
|
|
8
|
+
size?: number;
|
|
9
9
|
position?: SpinnerPosition;
|
|
10
|
+
/** Текст для скринридера. Виден только ему. */
|
|
11
|
+
label?: string;
|
|
10
12
|
}
|
|
11
13
|
export declare const Spinner: FC<SpinnerProps>;
|
|
12
14
|
export {};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
1
|
+
import { FC, HTMLAttributes } from "react";
|
|
2
2
|
declare const spinnerPosition: {
|
|
3
3
|
readonly top: "top";
|
|
4
4
|
readonly center: "center";
|
|
5
5
|
};
|
|
6
6
|
export type SpinnerPosition = (typeof spinnerPosition)[keyof typeof spinnerPosition];
|
|
7
|
-
export interface SpinnerProps {
|
|
8
|
-
|
|
7
|
+
export interface SpinnerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'size' | 'position'> {
|
|
8
|
+
size?: number;
|
|
9
9
|
position?: SpinnerPosition;
|
|
10
|
+
/** Текст для скринридера. Виден только ему. */
|
|
11
|
+
label?: string;
|
|
10
12
|
}
|
|
11
13
|
export declare const Spinner: FC<SpinnerProps>;
|
|
12
14
|
export {};
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { cn, when } from "../../lib/cn.js";
|
|
2
|
-
import {
|
|
2
|
+
import { useId, useSyncExternalStore } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
import "./spinner.scss";
|
|
4
5
|
// src/components/spinner/spinner.tsx
|
|
5
6
|
const spinnerPosition = {
|
|
6
7
|
top: "top",
|
|
7
8
|
center: "center"
|
|
8
9
|
};
|
|
10
|
+
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
|
|
11
|
+
const subscribeReducedMotion = (onChange) => {
|
|
12
|
+
if (typeof window.matchMedia !== "function") return () => {};
|
|
13
|
+
const media = window.matchMedia(reducedMotionQuery);
|
|
14
|
+
media.addEventListener("change", onChange);
|
|
15
|
+
return () => media.removeEventListener("change", onChange);
|
|
16
|
+
};
|
|
17
|
+
const prefersReducedMotion = () => typeof window.matchMedia === "function" && window.matchMedia(reducedMotionQuery).matches;
|
|
18
|
+
const prefersReducedMotionServer = () => false;
|
|
9
19
|
const dotDelays = [
|
|
10
20
|
"0s",
|
|
11
21
|
"0.125s",
|
|
@@ -16,12 +26,12 @@ const dotDelays = [
|
|
|
16
26
|
"0.75s",
|
|
17
27
|
"0.875s"
|
|
18
28
|
];
|
|
19
|
-
const Dot = ({ begin, rotate }) => /* @__PURE__ */ jsx("circle", {
|
|
29
|
+
const Dot = ({ begin, rotate, reduced }) => /* @__PURE__ */ jsx("circle", {
|
|
20
30
|
transform: `rotate(${rotate} 16 16)`,
|
|
21
31
|
cx: "16",
|
|
22
32
|
cy: "3",
|
|
23
|
-
r:
|
|
24
|
-
children: /* @__PURE__ */ jsx("animate", {
|
|
33
|
+
r: reduced ? 3 : 0,
|
|
34
|
+
children: reduced ? null : /* @__PURE__ */ jsx("animate", {
|
|
25
35
|
attributeName: "r",
|
|
26
36
|
values: "0;3;0;0",
|
|
27
37
|
dur: "1s",
|
|
@@ -34,21 +44,33 @@ const Dot = ({ begin, rotate }) => /* @__PURE__ */ jsx("circle", {
|
|
|
34
44
|
const svgProps = {
|
|
35
45
|
"xmlns": "http://www.w3.org/2000/svg",
|
|
36
46
|
"viewBox": "0 0 32 32",
|
|
37
|
-
"
|
|
38
|
-
"height": 64,
|
|
39
|
-
"fill": "#ecebeb",
|
|
47
|
+
"fill": "var(--color-muted)",
|
|
40
48
|
"aria-hidden": true
|
|
41
49
|
};
|
|
42
|
-
const Spinner = ({ className, position = spinnerPosition.center
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const Spinner = ({ className, position = spinnerPosition.center, size = 64, label = "Загрузка", ...props }) => {
|
|
51
|
+
const labelId = useId();
|
|
52
|
+
const reduced = useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, prefersReducedMotionServer);
|
|
53
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
...props,
|
|
55
|
+
role: "status",
|
|
56
|
+
"aria-labelledby": labelId,
|
|
57
|
+
className: cn("spinner", className, when(position === spinnerPosition.top, "spinner--top")),
|
|
58
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
59
|
+
id: labelId,
|
|
60
|
+
className: "spinner__label",
|
|
61
|
+
children: label
|
|
62
|
+
}), /* @__PURE__ */ jsx("svg", {
|
|
63
|
+
...svgProps,
|
|
64
|
+
width: size,
|
|
65
|
+
height: size,
|
|
66
|
+
children: dotDelays.map((begin, index) => /* @__PURE__ */ jsx(Dot, {
|
|
67
|
+
begin,
|
|
68
|
+
rotate: index * 45,
|
|
69
|
+
reduced
|
|
70
|
+
}, begin))
|
|
71
|
+
})]
|
|
72
|
+
});
|
|
73
|
+
};
|
|
52
74
|
export { Spinner };
|
|
53
75
|
|
|
54
76
|
//# sourceMappingURL=spinner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spinner.js","names":[],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import type
|
|
1
|
+
{"version":3,"file":"spinner.js","names":[],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\n useId,\n useSyncExternalStore,\n type FC,\n type HTMLAttributes,\n type SVGProps,\n} from 'react';\n\nimport { cn, when } from '@/lib/cn';\nimport './spinner.scss';\n\nconst spinnerPosition = {\n top: 'top',\n center: 'center',\n} as const;\n\nexport type SpinnerPosition =\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\n\nexport interface SpinnerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n 'size' | 'position'\n> {\n size?: number;\n position?: SpinnerPosition;\n /** Текст для скринридера. Виден только ему. */\n label?: string;\n}\n\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\n\nconst subscribeReducedMotion = (onChange: () => void) => {\n if (typeof window.matchMedia !== 'function') return () => {};\n\n const media = window.matchMedia(reducedMotionQuery);\n media.addEventListener('change', onChange);\n return () => media.removeEventListener('change', onChange);\n};\n\nconst prefersReducedMotion = () =>\n typeof window.matchMedia === 'function' &&\n window.matchMedia(reducedMotionQuery).matches;\n\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\nconst prefersReducedMotionServer = () => false;\n\nconst dotDelays = [\n '0s',\n '0.125s',\n '0.25s',\n '0.375s',\n '0.5s',\n '0.625s',\n '0.75s',\n '0.875s',\n] as const;\n\nconst Dot = ({\n begin,\n rotate,\n reduced,\n}: {\n begin: string;\n rotate: number;\n reduced: boolean;\n}) => (\n <circle\n transform={`rotate(${rotate} 16 16)`}\n cx=\"16\"\n cy=\"3\"\n r={reduced ? 3 : 0}\n >\n {reduced ? null : (\n <animate\n attributeName=\"r\"\n values=\"0;3;0;0\"\n dur=\"1s\"\n repeatCount=\"indefinite\"\n begin={begin}\n keySplines=\"0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8\"\n calcMode=\"spline\"\n />\n )}\n </circle>\n);\n\nconst svgProps: SVGProps<SVGSVGElement> = {\n 'xmlns': 'http://www.w3.org/2000/svg',\n 'viewBox': '0 0 32 32',\n 'fill': 'var(--color-muted)',\n 'aria-hidden': true,\n};\n\nexport const Spinner: FC<SpinnerProps> = ({\n className,\n position = spinnerPosition.center,\n size = 64,\n label = 'Загрузка',\n ...props\n}) => {\n const labelId = useId();\n const reduced = useSyncExternalStore(\n subscribeReducedMotion,\n prefersReducedMotion,\n prefersReducedMotionServer,\n );\n\n return (\n <div\n {...props}\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\n role=\"status\"\n aria-labelledby={labelId}\n className={cn(\n 'spinner',\n className,\n when(position === spinnerPosition.top, 'spinner--top'),\n )}\n >\n <span id={labelId} className=\"spinner__label\">\n {label}\n </span>\n <svg {...svgProps} width={size} height={size}>\n {dotDelays.map((begin, index) => (\n <Dot\n key={begin}\n begin={begin}\n rotate={index * 45}\n reduced={reduced}\n />\n ))}\n </svg>\n </div>\n );\n};\n"],"mappings":";;;;;AAWA,MAAM,kBAAkB;CACtB,KAAK;CACL,QAAQ;AACV;AAeA,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B,aAAyB;CACvD,IAAI,OAAO,OAAO,eAAe,YAAY,aAAa,CAAC;CAE3D,MAAM,QAAQ,OAAO,WAAW,kBAAkB;CAClD,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa,MAAM,oBAAoB,UAAU,QAAQ;AAC3D;AAEA,MAAM,6BACJ,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kBAAkB,CAAC,CAAC;AAGxC,MAAM,mCAAmC;AAEzC,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,OAAO,EACX,OACA,QACA,cAMA,oBAAC,UAAD;CACE,WAAW,UAAU,OAAO;CAC5B,IAAG;CACH,IAAG;CACH,GAAG,UAAU,IAAI;CAEhB,UAAA,UAAU,OACT,oBAAC,WAAD;EACE,eAAc;EACd,QAAO;EACP,KAAI;EACJ,aAAY;EACL;EACP,YAAW;EACX,UAAS;CACV,CAAA;AAEG,CAAA;AAGV,MAAM,WAAoC;CACxC,SAAS;CACT,WAAW;CACX,QAAQ;CACR,eAAe;AACjB;AAEA,MAAa,WAA6B,EACxC,WACA,WAAW,gBAAgB,QAC3B,OAAO,IACP,QAAQ,YACR,GAAG,YACC;CACJ,MAAM,UAAU,MAAM;CACtB,MAAM,UAAU,qBACd,wBACA,sBACA,0BACF;CAEA,OACE,qBAAC,OAAD;EACE,GAAI;EAEJ,MAAK;EACL,mBAAiB;EACjB,WAAW,GACT,WACA,WACA,KAAK,aAAa,gBAAgB,KAAK,cAAc,CACvD;EATF,UAAA,CAWE,oBAAC,QAAD;GAAM,IAAI;GAAS,WAAU;GAC1B,UAAA;EACG,CAAA,GACN,oBAAC,OAAD;GAAK,GAAI;GAAU,OAAO;GAAM,QAAQ;GACrC,UAAA,UAAU,KAAK,OAAO,UACrB,oBAAC,KAAD;IAES;IACP,QAAQ,QAAQ;IACP;GACV,GAJM,KAIN,CACF;EACE,CAAA,CACF;;AAET"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
// Блок во всю ширину родителя: position раскладывает svg по высоте,
|
|
2
|
+
// поэтому min-height обязателен — без него flex-start и center совпадают.
|
|
3
|
+
// position: relative держит внутри скрытую подпись.
|
|
1
4
|
.spinner {
|
|
5
|
+
position: relative;
|
|
6
|
+
box-sizing: border-box;
|
|
2
7
|
display: flex;
|
|
3
8
|
width: 100%;
|
|
4
9
|
min-height: 100%;
|
|
5
|
-
padding:
|
|
10
|
+
padding: var(--space-6) 0;
|
|
6
11
|
align-items: center;
|
|
7
12
|
justify-content: center;
|
|
8
13
|
|
|
@@ -10,3 +15,15 @@
|
|
|
10
15
|
align-items: flex-start;
|
|
11
16
|
}
|
|
12
17
|
}
|
|
18
|
+
|
|
19
|
+
.spinner__label {
|
|
20
|
+
position: absolute;
|
|
21
|
+
width: 1px;
|
|
22
|
+
height: 1px;
|
|
23
|
+
padding: 0;
|
|
24
|
+
margin: -1px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
clip-path: inset(50%);
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
border: 0;
|
|
29
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { TextArea, TextAreaProps } from "./textarea.cjs";
|
|
2
|
-
export { TextArea, type TextAreaProps };
|
|
1
|
+
import { TextArea, TextAreaProps, TextAreaSize } from "./textarea.cjs";
|
|
2
|
+
export { TextArea, type TextAreaProps, type TextAreaSize };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { TextArea, TextAreaProps } from "./textarea.js";
|
|
2
|
-
export { TextArea, type TextAreaProps };
|
|
1
|
+
import { TextArea, TextAreaProps, TextAreaSize } from "./textarea.js";
|
|
2
|
+
export { TextArea, type TextAreaProps, type TextAreaSize };
|
|
@@ -1,24 +1,83 @@
|
|
|
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("./textarea.scss");
|
|
5
6
|
// src/components/textarea/textarea.tsx
|
|
6
|
-
const
|
|
7
|
+
const assignRef = (ref, node) => {
|
|
8
|
+
if (typeof ref === "function") {
|
|
9
|
+
ref(node);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (ref) ref.current = node;
|
|
13
|
+
};
|
|
14
|
+
const TextArea = (0, react.forwardRef)(function TextArea({ maxLength, value, size = "md", recommended, error, counter = true, className, id, onChange, "aria-describedby": ariaDescribedBy, ...props }, ref) {
|
|
15
|
+
const autoId = (0, react.useId)();
|
|
16
|
+
const field = require_field_context.useFieldControl();
|
|
17
|
+
const reportValue = field?.reportValue;
|
|
18
|
+
const textareaId = field?.id ?? id ?? autoId;
|
|
19
|
+
const textareaRef = (0, react.useRef)(null);
|
|
20
|
+
const [length, setLength] = (0, react.useState)(0);
|
|
21
|
+
const withCounter = maxLength !== void 0 && counter;
|
|
22
|
+
const isInvalid = Boolean(error) || Boolean(field?.invalid);
|
|
23
|
+
const setRef = (0, react.useCallback)((node) => {
|
|
24
|
+
textareaRef.current = node;
|
|
25
|
+
assignRef(ref, node);
|
|
26
|
+
}, [ref]);
|
|
27
|
+
/**
|
|
28
|
+
* ponytail: длину счётчик читает из DOM — одинаково для `value` и `defaultValue`,
|
|
29
|
+
* включая автозаполнение. Потолок: reset формы в неконтролируемом режиме счётчик
|
|
30
|
+
* не слышит и догоняет на следующем вводе; апгрейд — слушать `reset`, как `Input`.
|
|
31
|
+
* Тот же проход отдаёт показанную строку в Field.
|
|
32
|
+
*/
|
|
33
|
+
(0, react.useEffect)(() => {
|
|
34
|
+
const node = textareaRef.current;
|
|
35
|
+
if (!node) return;
|
|
36
|
+
if (withCounter) setLength(node.value.length);
|
|
37
|
+
reportValue?.(node.value);
|
|
38
|
+
}, [
|
|
39
|
+
reportValue,
|
|
40
|
+
value,
|
|
41
|
+
withCounter
|
|
42
|
+
]);
|
|
43
|
+
const handleChange = (event) => {
|
|
44
|
+
if (withCounter) setLength(event.target.value.length);
|
|
45
|
+
if (value === void 0) reportValue?.(event.target.value);
|
|
46
|
+
onChange?.(event);
|
|
47
|
+
};
|
|
48
|
+
const counterId = withCounter ? `${textareaId}-counter` : void 0;
|
|
49
|
+
const describedBy = [
|
|
50
|
+
field?.describedBy,
|
|
51
|
+
counterId,
|
|
52
|
+
ariaDescribedBy
|
|
53
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
7
54
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8
55
|
className: "textarea__wrap",
|
|
9
56
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
57
|
+
...props,
|
|
58
|
+
ref: setRef,
|
|
59
|
+
id: textareaId,
|
|
60
|
+
className: require_cn.cn("textarea", className, require_cn.when(size !== "md", `textarea--${size}`), require_cn.when(recommended, "textarea--recommended"), require_cn.when(isInvalid, "textarea--error")),
|
|
61
|
+
...value !== void 0 ? { value } : {},
|
|
13
62
|
maxLength,
|
|
14
|
-
|
|
15
|
-
...
|
|
16
|
-
|
|
63
|
+
onChange: handleChange,
|
|
64
|
+
...isInvalid ? { "aria-invalid": true } : {},
|
|
65
|
+
...field?.required ? {
|
|
66
|
+
"required": true,
|
|
67
|
+
"aria-required": true
|
|
68
|
+
} : {},
|
|
69
|
+
...describedBy ? { "aria-describedby": describedBy } : {},
|
|
70
|
+
...field?.errorId ? { "aria-errormessage": field.errorId } : {}
|
|
71
|
+
}), withCounter ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
72
|
+
id: counterId,
|
|
17
73
|
className: "textarea__counter",
|
|
18
74
|
children: [
|
|
19
|
-
"
|
|
75
|
+
"Осталось ",
|
|
76
|
+
Math.max(0, (maxLength ?? 0) - length),
|
|
77
|
+
" из ",
|
|
20
78
|
maxLength,
|
|
21
|
-
"
|
|
79
|
+
" ",
|
|
80
|
+
"символов"
|
|
22
81
|
]
|
|
23
82
|
}) : null]
|
|
24
83
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.cjs","names":["forwardRef","cn","when"],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"textarea.cjs","names":["forwardRef","useId","useFieldControl","useRef","useState","useCallback","cn","when"],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ChangeEvent,\n type Ref,\n type TextareaHTMLAttributes,\n} from 'react';\n\nimport { useFieldControl } from '@/components/field/field-context';\nimport { cn, when } from '@/lib/cn';\nimport './textarea.scss';\n\nexport type TextAreaSize = 'sm' | 'md' | 'lg';\n\nexport interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n size?: TextAreaSize;\n recommended?: boolean;\n error?: boolean;\n /** Счётчик остатка при `maxLength`. `false` — то, что в приложении было `noMaxMoty`. */\n counter?: boolean;\n}\n\nconst assignRef = (\n ref: Ref<HTMLTextAreaElement> | undefined,\n node: HTMLTextAreaElement | null,\n) => {\n if (typeof ref === 'function') {\n ref(node);\n return;\n }\n\n if (ref) ref.current = node;\n};\n\nexport const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(\n function TextArea(\n {\n maxLength,\n value,\n size = 'md',\n recommended,\n error,\n counter = true,\n className,\n id,\n onChange,\n 'aria-describedby': ariaDescribedBy,\n ...props\n },\n ref,\n ) {\n const autoId = useId();\n const field = useFieldControl();\n const reportValue = field?.reportValue;\n const textareaId = field?.id ?? id ?? autoId;\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const [length, setLength] = useState(0);\n const withCounter = maxLength !== undefined && counter;\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\n\n const setRef = useCallback(\n (node: HTMLTextAreaElement | null) => {\n textareaRef.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n\n /**\n * ponytail: длину счётчик читает из DOM — одинаково для `value` и `defaultValue`,\n * включая автозаполнение. Потолок: reset формы в неконтролируемом режиме счётчик\n * не слышит и догоняет на следующем вводе; апгрейд — слушать `reset`, как `Input`.\n * Тот же проход отдаёт показанную строку в Field.\n */\n useEffect(() => {\n const node = textareaRef.current;\n if (!node) return;\n\n if (withCounter) setLength(node.value.length);\n reportValue?.(node.value);\n }, [reportValue, value, withCounter]);\n\n const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {\n if (withCounter) setLength(event.target.value.length);\n if (value === undefined) reportValue?.(event.target.value);\n onChange?.(event);\n };\n\n const counterId = withCounter ? `${textareaId}-counter` : undefined;\n const describedBy = [field?.describedBy, counterId, ariaDescribedBy]\n .filter(part => Boolean(part))\n .join(' ');\n\n return (\n <div className=\"textarea__wrap\">\n <textarea\n {...props}\n ref={setRef}\n id={textareaId}\n className={cn(\n 'textarea',\n className,\n when(size !== 'md', `textarea--${size}`),\n when(recommended, 'textarea--recommended'),\n when(isInvalid, 'textarea--error'),\n )}\n {...(value !== undefined ? { value } : {})}\n maxLength={maxLength}\n onChange={handleChange}\n {...(isInvalid ? { 'aria-invalid': true } : {})}\n {...(field?.required\n ? { 'required': true, 'aria-required': true }\n : {})}\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\n />\n {withCounter ? (\n <span id={counterId} className=\"textarea__counter\">\n Осталось {Math.max(0, (maxLength ?? 0) - length)} из {maxLength}{' '}\n символов\n </span>\n ) : null}\n </div>\n );\n },\n);\n"],"mappings":";;;;;;AA0BA,MAAM,aACJ,KACA,SACG;CACH,IAAI,OAAO,QAAQ,YAAY;EAC7B,IAAI,IAAI;EACR;CACF;CAEA,IAAI,KAAK,IAAI,UAAU;AACzB;AAEA,MAAa,YAAA,GAAWA,MAAAA,WAAAA,CACtB,SAAS,SACP,EACE,WACA,OACA,OAAO,MACP,aACA,OACA,UAAU,MACV,WACA,IACA,UACA,oBAAoB,iBACpB,GAAG,SAEL,KACA;CACA,MAAM,UAAA,GAASC,MAAAA,MAAAA,CAAM;CACrB,MAAM,QAAQC,sBAAAA,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,aAAa,OAAO,MAAM,MAAM;CACtC,MAAM,eAAA,GAAcC,MAAAA,OAAAA,CAA4B,IAAI;CACpD,MAAM,CAAC,QAAQ,cAAA,GAAaC,MAAAA,SAAAA,CAAS,CAAC;CACtC,MAAM,cAAc,cAAc,KAAA,KAAa;CAC/C,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAE1D,MAAM,UAAA,GAASC,MAAAA,YAAAA,EACZ,SAAqC;EACpC,YAAY,UAAU;EACtB,UAAU,KAAK,IAAI;CACrB,GACA,CAAC,GAAG,CACN;;;;;;;CAQA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,OAAO,YAAY;EACzB,IAAI,CAAC,MAAM;EAEX,IAAI,aAAa,UAAU,KAAK,MAAM,MAAM;EAC5C,cAAc,KAAK,KAAK;CAC1B,GAAG;EAAC;EAAa;EAAO;CAAW,CAAC;CAEpC,MAAM,gBAAgB,UAA4C;EAChE,IAAI,aAAa,UAAU,MAAM,OAAO,MAAM,MAAM;EACpD,IAAI,UAAU,KAAA,GAAW,cAAc,MAAM,OAAO,KAAK;EACzD,WAAW,KAAK;CAClB;CAEA,MAAM,YAAY,cAAc,GAAG,WAAW,YAAY,KAAA;CAC1D,MAAM,cAAc;EAAC,OAAO;EAAa;EAAW;CAAe,CAAC,CACjE,QAAO,SAAQ,QAAQ,IAAI,CAAC,CAAC,CAC7B,KAAK,GAAG;CAEX,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,YAAD;GACE,GAAI;GACJ,KAAK;GACL,IAAI;GACJ,WAAWC,WAAAA,GACT,YACA,WACAC,WAAAA,KAAK,SAAS,MAAM,aAAa,MAAM,GACvCA,WAAAA,KAAK,aAAa,uBAAuB,GACzCA,WAAAA,KAAK,WAAW,iBAAiB,CACnC;GACA,GAAK,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GAC7B;GACX,UAAU;GACV,GAAK,YAAY,EAAE,gBAAgB,KAAK,IAAI,CAAC;GAC7C,GAAK,OAAO,WACR;IAAE,YAAY;IAAM,iBAAiB;GAAK,IAC1C,CAAC;GACL,GAAK,cAAc,EAAE,oBAAoB,YAAY,IAAI,CAAC;GAC1D,GAAK,OAAO,UAAU,EAAE,qBAAqB,MAAM,QAAQ,IAAI,CAAC;EACjE,CAAA,GACA,cACC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;GAAM,IAAI;GAAW,WAAU;GAA/B,UAAA;IAAmD;IACvC,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM;IAAE;IAAK;IAAW;IAAI;GAEjE;EACJ,CAAA,IAAA,IACD;;AAET,CACF"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TextareaHTMLAttributes } from "react";
|
|
2
|
+
export type TextAreaSize = 'sm' | 'md' | 'lg';
|
|
2
3
|
export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
size?: TextAreaSize;
|
|
3
5
|
recommended?: boolean;
|
|
4
6
|
error?: boolean;
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
+
/** Счётчик остатка при `maxLength`. `false` — то, что в приложении было `noMaxMoty`. */
|
|
8
|
+
counter?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare const TextArea: import("react").ForwardRefExoticComponent<TextAreaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
9
11
|
//# sourceMappingURL=textarea.d.cts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TextareaHTMLAttributes } from "react";
|
|
2
|
+
export type TextAreaSize = 'sm' | 'md' | 'lg';
|
|
2
3
|
export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
size?: TextAreaSize;
|
|
3
5
|
recommended?: boolean;
|
|
4
6
|
error?: boolean;
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
+
/** Счётчик остатка при `maxLength`. `false` — то, что в приложении было `noMaxMoty`. */
|
|
8
|
+
counter?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare const TextArea: import("react").ForwardRefExoticComponent<TextAreaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
9
11
|
//# sourceMappingURL=textarea.d.ts.map
|