@de-kit/st-ui 0.1.6 → 0.1.8
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 +143 -143
- package/dist/components/button/button.cjs +4 -3
- package/dist/components/button/button.cjs.map +1 -1
- package/dist/components/button/button.d.cts +5 -3
- package/dist/components/button/button.d.ts +5 -3
- package/dist/components/button/button.js +4 -3
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/button.scss +147 -148
- package/dist/components/checkbox/checkbox.cjs.map +1 -1
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox/checkbox.scss +137 -137
- package/dist/components/field/field-context.cjs.map +1 -1
- package/dist/components/field/field-context.js.map +1 -1
- package/dist/components/field/field.cjs.map +1 -1
- package/dist/components/field/field.js.map +1 -1
- package/dist/components/field/field.scss +43 -43
- package/dist/components/input/input.cjs.map +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.scss +127 -127
- package/dist/components/radio/radio.cjs.map +1 -1
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/radio/radio.scss +144 -144
- package/dist/components/spinner/spinner.cjs.map +1 -1
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/spinner/spinner.scss +29 -29
- package/dist/components/textarea/textarea.cjs.map +1 -1
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/textarea/textarea.scss +85 -85
- package/dist/icons/icon-label-ozon.cjs +4 -4
- package/dist/icons/icon-label-ozon.cjs.map +1 -1
- package/dist/icons/icon-label-ozon.js +4 -4
- package/dist/icons/icon-label-ozon.js.map +1 -1
- package/dist/icons/icon-label-yandex.cjs +1 -1
- package/dist/icons/icon-label-yandex.cjs.map +1 -1
- package/dist/icons/icon-label-yandex.js +1 -1
- package/dist/icons/icon-label-yandex.js.map +1 -1
- package/dist/icons/svg.cjs.map +1 -1
- package/dist/icons/svg.js.map +1 -1
- package/dist/lib/cn.cjs.map +1 -1
- package/dist/lib/cn.js.map +1 -1
- package/dist/styles/global.scss +201 -201
- package/dist/styles/mixins.scss +51 -51
- package/dist/styles/vars.scss +87 -87
- package/package.json +1 -1
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
.radio__label {
|
|
2
|
-
font-size: var(--text-sm);
|
|
3
|
-
line-height: var(--text-sm--line-height);
|
|
4
|
-
display: inline-block;
|
|
5
|
-
vertical-align: top;
|
|
6
|
-
padding-left: var(--space-1);
|
|
7
|
-
user-select: none;
|
|
8
|
-
cursor: pointer;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.radio__input {
|
|
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
|
-
box-sizing: border-box;
|
|
21
|
-
appearance: none;
|
|
22
|
-
width: 20px;
|
|
23
|
-
height: 20px;
|
|
24
|
-
outline: none;
|
|
25
|
-
display: inline-block;
|
|
26
|
-
vertical-align: top;
|
|
27
|
-
position: relative;
|
|
28
|
-
margin: 0;
|
|
29
|
-
flex-shrink: 0;
|
|
30
|
-
cursor: pointer;
|
|
31
|
-
border: 1px solid var(--bc, var(--border));
|
|
32
|
-
background: var(--b, var(--background));
|
|
33
|
-
border-radius: 50%;
|
|
34
|
-
transition:
|
|
35
|
-
background 0.3s,
|
|
36
|
-
border-color 0.3s,
|
|
37
|
-
box-shadow 0.2s;
|
|
38
|
-
|
|
39
|
-
&::after {
|
|
40
|
-
content: '';
|
|
41
|
-
display: block;
|
|
42
|
-
width: 6px;
|
|
43
|
-
height: 6px;
|
|
44
|
-
left: 50%;
|
|
45
|
-
top: 50%;
|
|
46
|
-
border-radius: 50%;
|
|
47
|
-
background: var(--active-inner);
|
|
48
|
-
position: absolute;
|
|
49
|
-
opacity: var(--o, 0);
|
|
50
|
-
transform: translate(-50%, -50%);
|
|
51
|
-
transition:
|
|
52
|
-
transform var(--d-t, 0.3s) var(--d-t-e, ease),
|
|
53
|
-
opacity var(--d-o, 0.2s);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
&:checked {
|
|
57
|
-
--b: var(--active);
|
|
58
|
-
--bc: var(--active);
|
|
59
|
-
--o: 1;
|
|
60
|
-
--d-o: 0.3s;
|
|
61
|
-
--d-t: 0.6s;
|
|
62
|
-
--d-t-e: cubic-bezier(0.2, 0.85, 0.32, 1.2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
&:hover:not(:checked):not(:disabled) {
|
|
66
|
-
border-width: 2px;
|
|
67
|
-
--bc: var(--active);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
&:hover:not(:disabled) {
|
|
71
|
-
border-color: var(--border-hover);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
&:focus {
|
|
75
|
-
border: 1px solid var(--border-hover);
|
|
76
|
-
}
|
|
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
|
-
|
|
87
|
-
&:disabled {
|
|
88
|
-
--b: var(--disabled);
|
|
89
|
-
cursor: not-allowed;
|
|
90
|
-
opacity: 1;
|
|
91
|
-
|
|
92
|
-
&:checked {
|
|
93
|
-
--b: var(--disabled-inner);
|
|
94
|
-
--bc: var(--border);
|
|
95
|
-
|
|
96
|
-
&::after {
|
|
97
|
-
background: var(--disabled-after-inner);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
& + .radio__label {
|
|
102
|
-
color: var(--color-muted-foreground);
|
|
103
|
-
cursor: not-allowed;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Модификатор ошибки после .radio__input: иначе shorthand border затрёт border-color.
|
|
109
|
-
.radio {
|
|
110
|
-
display: flex;
|
|
111
|
-
align-items: flex-start;
|
|
112
|
-
|
|
113
|
-
&--error {
|
|
114
|
-
.radio__input {
|
|
115
|
-
border-color: color-mix(
|
|
116
|
-
in srgb,
|
|
117
|
-
var(--color-destructive) 50%,
|
|
118
|
-
transparent
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
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
|
+
.radio__label {
|
|
2
|
+
font-size: var(--text-sm);
|
|
3
|
+
line-height: var(--text-sm--line-height);
|
|
4
|
+
display: inline-block;
|
|
5
|
+
vertical-align: top;
|
|
6
|
+
padding-left: var(--space-1);
|
|
7
|
+
user-select: none;
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.radio__input {
|
|
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
|
+
box-sizing: border-box;
|
|
21
|
+
appearance: none;
|
|
22
|
+
width: 20px;
|
|
23
|
+
height: 20px;
|
|
24
|
+
outline: none;
|
|
25
|
+
display: inline-block;
|
|
26
|
+
vertical-align: top;
|
|
27
|
+
position: relative;
|
|
28
|
+
margin: 0;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
border: 1px solid var(--bc, var(--border));
|
|
32
|
+
background: var(--b, var(--background));
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
transition:
|
|
35
|
+
background 0.3s,
|
|
36
|
+
border-color 0.3s,
|
|
37
|
+
box-shadow 0.2s;
|
|
38
|
+
|
|
39
|
+
&::after {
|
|
40
|
+
content: '';
|
|
41
|
+
display: block;
|
|
42
|
+
width: 6px;
|
|
43
|
+
height: 6px;
|
|
44
|
+
left: 50%;
|
|
45
|
+
top: 50%;
|
|
46
|
+
border-radius: 50%;
|
|
47
|
+
background: var(--active-inner);
|
|
48
|
+
position: absolute;
|
|
49
|
+
opacity: var(--o, 0);
|
|
50
|
+
transform: translate(-50%, -50%);
|
|
51
|
+
transition:
|
|
52
|
+
transform var(--d-t, 0.3s) var(--d-t-e, ease),
|
|
53
|
+
opacity var(--d-o, 0.2s);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:checked {
|
|
57
|
+
--b: var(--active);
|
|
58
|
+
--bc: var(--active);
|
|
59
|
+
--o: 1;
|
|
60
|
+
--d-o: 0.3s;
|
|
61
|
+
--d-t: 0.6s;
|
|
62
|
+
--d-t-e: cubic-bezier(0.2, 0.85, 0.32, 1.2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&:hover:not(:checked):not(:disabled) {
|
|
66
|
+
border-width: 2px;
|
|
67
|
+
--bc: var(--active);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&:hover:not(:disabled) {
|
|
71
|
+
border-color: var(--border-hover);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:focus {
|
|
75
|
+
border: 1px solid var(--border-hover);
|
|
76
|
+
}
|
|
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
|
+
|
|
87
|
+
&:disabled {
|
|
88
|
+
--b: var(--disabled);
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
opacity: 1;
|
|
91
|
+
|
|
92
|
+
&:checked {
|
|
93
|
+
--b: var(--disabled-inner);
|
|
94
|
+
--bc: var(--border);
|
|
95
|
+
|
|
96
|
+
&::after {
|
|
97
|
+
background: var(--disabled-after-inner);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
& + .radio__label {
|
|
102
|
+
color: var(--color-muted-foreground);
|
|
103
|
+
cursor: not-allowed;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Модификатор ошибки после .radio__input: иначе shorthand border затрёт border-color.
|
|
109
|
+
.radio {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: flex-start;
|
|
112
|
+
|
|
113
|
+
&--error {
|
|
114
|
+
.radio__input {
|
|
115
|
+
border-color: color-mix(
|
|
116
|
+
in srgb,
|
|
117
|
+
var(--color-destructive) 50%,
|
|
118
|
+
transparent
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
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 +1 @@
|
|
|
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
|
+
{"version":3,"file":"spinner.cjs","names":["useId","useSyncExternalStore","cn","when"],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\r\n useId,\r\n useSyncExternalStore,\r\n type FC,\r\n type HTMLAttributes,\r\n type SVGProps,\r\n} from 'react';\r\n\r\nimport { cn, when } from '@/lib/cn';\r\nimport './spinner.scss';\r\n\r\nconst spinnerPosition = {\r\n top: 'top',\r\n center: 'center',\r\n} as const;\r\n\r\nexport type SpinnerPosition =\r\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\r\n\r\nexport interface SpinnerProps extends Omit<\r\n HTMLAttributes<HTMLDivElement>,\r\n 'size' | 'position'\r\n> {\r\n size?: number;\r\n position?: SpinnerPosition;\r\n /** Текст для скринридера. Виден только ему. */\r\n label?: string;\r\n}\r\n\r\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\r\n\r\nconst subscribeReducedMotion = (onChange: () => void) => {\r\n if (typeof window.matchMedia !== 'function') return () => {};\r\n\r\n const media = window.matchMedia(reducedMotionQuery);\r\n media.addEventListener('change', onChange);\r\n return () => media.removeEventListener('change', onChange);\r\n};\r\n\r\nconst prefersReducedMotion = () =>\r\n typeof window.matchMedia === 'function' &&\r\n window.matchMedia(reducedMotionQuery).matches;\r\n\r\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\r\nconst prefersReducedMotionServer = () => false;\r\n\r\nconst dotDelays = [\r\n '0s',\r\n '0.125s',\r\n '0.25s',\r\n '0.375s',\r\n '0.5s',\r\n '0.625s',\r\n '0.75s',\r\n '0.875s',\r\n] as const;\r\n\r\nconst Dot = ({\r\n begin,\r\n rotate,\r\n reduced,\r\n}: {\r\n begin: string;\r\n rotate: number;\r\n reduced: boolean;\r\n}) => (\r\n <circle\r\n transform={`rotate(${rotate} 16 16)`}\r\n cx=\"16\"\r\n cy=\"3\"\r\n r={reduced ? 3 : 0}\r\n >\r\n {reduced ? null : (\r\n <animate\r\n attributeName=\"r\"\r\n values=\"0;3;0;0\"\r\n dur=\"1s\"\r\n repeatCount=\"indefinite\"\r\n begin={begin}\r\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\"\r\n calcMode=\"spline\"\r\n />\r\n )}\r\n </circle>\r\n);\r\n\r\nconst svgProps: SVGProps<SVGSVGElement> = {\r\n 'xmlns': 'http://www.w3.org/2000/svg',\r\n 'viewBox': '0 0 32 32',\r\n 'fill': 'var(--color-muted)',\r\n 'aria-hidden': true,\r\n};\r\n\r\nexport const Spinner: FC<SpinnerProps> = ({\r\n className,\r\n position = spinnerPosition.center,\r\n size = 64,\r\n label = 'Загрузка',\r\n ...props\r\n}) => {\r\n const labelId = useId();\r\n const reduced = useSyncExternalStore(\r\n subscribeReducedMotion,\r\n prefersReducedMotion,\r\n prefersReducedMotionServer,\r\n );\r\n\r\n return (\r\n <div\r\n {...props}\r\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\r\n role=\"status\"\r\n aria-labelledby={labelId}\r\n className={cn(\r\n 'spinner',\r\n className,\r\n when(position === spinnerPosition.top, 'spinner--top'),\r\n )}\r\n >\r\n <span id={labelId} className=\"spinner__label\">\r\n {label}\r\n </span>\r\n <svg {...svgProps} width={size} height={size}>\r\n {dotDelays.map((begin, index) => (\r\n <Dot\r\n key={begin}\r\n begin={begin}\r\n rotate={index * 45}\r\n reduced={reduced}\r\n />\r\n ))}\r\n </svg>\r\n </div>\r\n );\r\n};\r\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 +1 @@
|
|
|
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
|
+
{"version":3,"file":"spinner.js","names":[],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import {\r\n useId,\r\n useSyncExternalStore,\r\n type FC,\r\n type HTMLAttributes,\r\n type SVGProps,\r\n} from 'react';\r\n\r\nimport { cn, when } from '@/lib/cn';\r\nimport './spinner.scss';\r\n\r\nconst spinnerPosition = {\r\n top: 'top',\r\n center: 'center',\r\n} as const;\r\n\r\nexport type SpinnerPosition =\r\n (typeof spinnerPosition)[keyof typeof spinnerPosition];\r\n\r\nexport interface SpinnerProps extends Omit<\r\n HTMLAttributes<HTMLDivElement>,\r\n 'size' | 'position'\r\n> {\r\n size?: number;\r\n position?: SpinnerPosition;\r\n /** Текст для скринридера. Виден только ему. */\r\n label?: string;\r\n}\r\n\r\nconst reducedMotionQuery = '(prefers-reduced-motion: reduce)';\r\n\r\nconst subscribeReducedMotion = (onChange: () => void) => {\r\n if (typeof window.matchMedia !== 'function') return () => {};\r\n\r\n const media = window.matchMedia(reducedMotionQuery);\r\n media.addEventListener('change', onChange);\r\n return () => media.removeEventListener('change', onChange);\r\n};\r\n\r\nconst prefersReducedMotion = () =>\r\n typeof window.matchMedia === 'function' &&\r\n window.matchMedia(reducedMotionQuery).matches;\r\n\r\n// ponytail: сервер всегда false — гидрация не расходится, но первый кадр после неё может мигнуть SMIL, пока клиент не прочитает matchMedia. Апгрейд: Sec-CH-Prefers-Reduced-Motion.\r\nconst prefersReducedMotionServer = () => false;\r\n\r\nconst dotDelays = [\r\n '0s',\r\n '0.125s',\r\n '0.25s',\r\n '0.375s',\r\n '0.5s',\r\n '0.625s',\r\n '0.75s',\r\n '0.875s',\r\n] as const;\r\n\r\nconst Dot = ({\r\n begin,\r\n rotate,\r\n reduced,\r\n}: {\r\n begin: string;\r\n rotate: number;\r\n reduced: boolean;\r\n}) => (\r\n <circle\r\n transform={`rotate(${rotate} 16 16)`}\r\n cx=\"16\"\r\n cy=\"3\"\r\n r={reduced ? 3 : 0}\r\n >\r\n {reduced ? null : (\r\n <animate\r\n attributeName=\"r\"\r\n values=\"0;3;0;0\"\r\n dur=\"1s\"\r\n repeatCount=\"indefinite\"\r\n begin={begin}\r\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\"\r\n calcMode=\"spline\"\r\n />\r\n )}\r\n </circle>\r\n);\r\n\r\nconst svgProps: SVGProps<SVGSVGElement> = {\r\n 'xmlns': 'http://www.w3.org/2000/svg',\r\n 'viewBox': '0 0 32 32',\r\n 'fill': 'var(--color-muted)',\r\n 'aria-hidden': true,\r\n};\r\n\r\nexport const Spinner: FC<SpinnerProps> = ({\r\n className,\r\n position = spinnerPosition.center,\r\n size = 64,\r\n label = 'Загрузка',\r\n ...props\r\n}) => {\r\n const labelId = useId();\r\n const reduced = useSyncExternalStore(\r\n subscribeReducedMotion,\r\n prefersReducedMotion,\r\n prefersReducedMotionServer,\r\n );\r\n\r\n return (\r\n <div\r\n {...props}\r\n // eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- live region, не <output>\r\n role=\"status\"\r\n aria-labelledby={labelId}\r\n className={cn(\r\n 'spinner',\r\n className,\r\n when(position === spinnerPosition.top, 'spinner--top'),\r\n )}\r\n >\r\n <span id={labelId} className=\"spinner__label\">\r\n {label}\r\n </span>\r\n <svg {...svgProps} width={size} height={size}>\r\n {dotDelays.map((begin, index) => (\r\n <Dot\r\n key={begin}\r\n begin={begin}\r\n rotate={index * 45}\r\n reduced={reduced}\r\n />\r\n ))}\r\n </svg>\r\n </div>\r\n );\r\n};\r\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,29 +1,29 @@
|
|
|
1
|
-
// Блок во всю ширину родителя: position раскладывает svg по высоте,
|
|
2
|
-
// поэтому min-height обязателен — без него flex-start и center совпадают.
|
|
3
|
-
// position: relative держит внутри скрытую подпись.
|
|
4
|
-
.spinner {
|
|
5
|
-
position: relative;
|
|
6
|
-
box-sizing: border-box;
|
|
7
|
-
display: flex;
|
|
8
|
-
width: 100%;
|
|
9
|
-
min-height: 100%;
|
|
10
|
-
padding: var(--space-6) 0;
|
|
11
|
-
align-items: center;
|
|
12
|
-
justify-content: center;
|
|
13
|
-
|
|
14
|
-
&--top {
|
|
15
|
-
align-items: flex-start;
|
|
16
|
-
}
|
|
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
|
+
// Блок во всю ширину родителя: position раскладывает svg по высоте,
|
|
2
|
+
// поэтому min-height обязателен — без него flex-start и center совпадают.
|
|
3
|
+
// position: relative держит внутри скрытую подпись.
|
|
4
|
+
.spinner {
|
|
5
|
+
position: relative;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
display: flex;
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-height: 100%;
|
|
10
|
+
padding: var(--space-6) 0;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
|
|
14
|
+
&--top {
|
|
15
|
+
align-items: flex-start;
|
|
16
|
+
}
|
|
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 +1 @@
|
|
|
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
|
+
{"version":3,"file":"textarea.cjs","names":["forwardRef","useId","useFieldControl","useRef","useState","useCallback","cn","when"],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["import {\r\n forwardRef,\r\n useCallback,\r\n useEffect,\r\n useId,\r\n useRef,\r\n useState,\r\n type ChangeEvent,\r\n type Ref,\r\n type TextareaHTMLAttributes,\r\n} from 'react';\r\n\r\nimport { useFieldControl } from '@/components/field/field-context';\r\nimport { cn, when } from '@/lib/cn';\r\nimport './textarea.scss';\r\n\r\nexport type TextAreaSize = 'sm' | 'md' | 'lg';\r\n\r\nexport interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\r\n size?: TextAreaSize;\r\n recommended?: boolean;\r\n error?: boolean;\r\n /** Счётчик остатка при `maxLength`. `false` — то, что в приложении было `noMaxMoty`. */\r\n counter?: boolean;\r\n}\r\n\r\nconst assignRef = (\r\n ref: Ref<HTMLTextAreaElement> | undefined,\r\n node: HTMLTextAreaElement | null,\r\n) => {\r\n if (typeof ref === 'function') {\r\n ref(node);\r\n return;\r\n }\r\n\r\n if (ref) ref.current = node;\r\n};\r\n\r\nexport const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(\r\n function TextArea(\r\n {\r\n maxLength,\r\n value,\r\n size = 'md',\r\n recommended,\r\n error,\r\n counter = true,\r\n className,\r\n id,\r\n onChange,\r\n 'aria-describedby': ariaDescribedBy,\r\n ...props\r\n },\r\n ref,\r\n ) {\r\n const autoId = useId();\r\n const field = useFieldControl();\r\n const reportValue = field?.reportValue;\r\n const textareaId = field?.id ?? id ?? autoId;\r\n const textareaRef = useRef<HTMLTextAreaElement>(null);\r\n const [length, setLength] = useState(0);\r\n const withCounter = maxLength !== undefined && counter;\r\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\r\n\r\n const setRef = useCallback(\r\n (node: HTMLTextAreaElement | null) => {\r\n textareaRef.current = node;\r\n assignRef(ref, node);\r\n },\r\n [ref],\r\n );\r\n\r\n /**\r\n * ponytail: длину счётчик читает из DOM — одинаково для `value` и `defaultValue`,\r\n * включая автозаполнение. Потолок: reset формы в неконтролируемом режиме счётчик\r\n * не слышит и догоняет на следующем вводе; апгрейд — слушать `reset`, как `Input`.\r\n * Тот же проход отдаёт показанную строку в Field.\r\n */\r\n useEffect(() => {\r\n const node = textareaRef.current;\r\n if (!node) return;\r\n\r\n if (withCounter) setLength(node.value.length);\r\n reportValue?.(node.value);\r\n }, [reportValue, value, withCounter]);\r\n\r\n const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {\r\n if (withCounter) setLength(event.target.value.length);\r\n if (value === undefined) reportValue?.(event.target.value);\r\n onChange?.(event);\r\n };\r\n\r\n const counterId = withCounter ? `${textareaId}-counter` : undefined;\r\n const describedBy = [field?.describedBy, counterId, ariaDescribedBy]\r\n .filter(part => Boolean(part))\r\n .join(' ');\r\n\r\n return (\r\n <div className=\"textarea__wrap\">\r\n <textarea\r\n {...props}\r\n ref={setRef}\r\n id={textareaId}\r\n className={cn(\r\n 'textarea',\r\n className,\r\n when(size !== 'md', `textarea--${size}`),\r\n when(recommended, 'textarea--recommended'),\r\n when(isInvalid, 'textarea--error'),\r\n )}\r\n {...(value !== undefined ? { value } : {})}\r\n maxLength={maxLength}\r\n onChange={handleChange}\r\n {...(isInvalid ? { 'aria-invalid': true } : {})}\r\n {...(field?.required\r\n ? { 'required': true, 'aria-required': true }\r\n : {})}\r\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\r\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\r\n />\r\n {withCounter ? (\r\n <span id={counterId} className=\"textarea__counter\">\r\n Осталось {Math.max(0, (maxLength ?? 0) - length)} из {maxLength}{' '}\r\n символов\r\n </span>\r\n ) : null}\r\n </div>\r\n );\r\n },\r\n);\r\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.js","names":[],"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,WAAW,WACtB,SAAS,SACP,EACE,WACA,OACA,OAAO,MACP,aACA,OACA,UAAU,MACV,WACA,IACA,UACA,oBAAoB,iBACpB,GAAG,SAEL,KACA;CACA,MAAM,SAAS,MAAM;CACrB,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,aAAa,OAAO,MAAM,MAAM;CACtC,MAAM,cAAc,OAA4B,IAAI;CACpD,MAAM,CAAC,QAAQ,aAAa,SAAS,CAAC;CACtC,MAAM,cAAc,cAAc,KAAA,KAAa;CAC/C,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAE1D,MAAM,SAAS,aACZ,SAAqC;EACpC,YAAY,UAAU;EACtB,UAAU,KAAK,IAAI;CACrB,GACA,CAAC,GAAG,CACN;;;;;;;CAQA,gBAAgB;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,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,YAAD;GACE,GAAI;GACJ,KAAK;GACL,IAAI;GACJ,WAAW,GACT,YACA,WACA,KAAK,SAAS,MAAM,aAAa,MAAM,GACvC,KAAK,aAAa,uBAAuB,GACzC,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,qBAAC,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
|
+
{"version":3,"file":"textarea.js","names":[],"sources":["../../../src/components/textarea/textarea.tsx"],"sourcesContent":["import {\r\n forwardRef,\r\n useCallback,\r\n useEffect,\r\n useId,\r\n useRef,\r\n useState,\r\n type ChangeEvent,\r\n type Ref,\r\n type TextareaHTMLAttributes,\r\n} from 'react';\r\n\r\nimport { useFieldControl } from '@/components/field/field-context';\r\nimport { cn, when } from '@/lib/cn';\r\nimport './textarea.scss';\r\n\r\nexport type TextAreaSize = 'sm' | 'md' | 'lg';\r\n\r\nexport interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\r\n size?: TextAreaSize;\r\n recommended?: boolean;\r\n error?: boolean;\r\n /** Счётчик остатка при `maxLength`. `false` — то, что в приложении было `noMaxMoty`. */\r\n counter?: boolean;\r\n}\r\n\r\nconst assignRef = (\r\n ref: Ref<HTMLTextAreaElement> | undefined,\r\n node: HTMLTextAreaElement | null,\r\n) => {\r\n if (typeof ref === 'function') {\r\n ref(node);\r\n return;\r\n }\r\n\r\n if (ref) ref.current = node;\r\n};\r\n\r\nexport const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(\r\n function TextArea(\r\n {\r\n maxLength,\r\n value,\r\n size = 'md',\r\n recommended,\r\n error,\r\n counter = true,\r\n className,\r\n id,\r\n onChange,\r\n 'aria-describedby': ariaDescribedBy,\r\n ...props\r\n },\r\n ref,\r\n ) {\r\n const autoId = useId();\r\n const field = useFieldControl();\r\n const reportValue = field?.reportValue;\r\n const textareaId = field?.id ?? id ?? autoId;\r\n const textareaRef = useRef<HTMLTextAreaElement>(null);\r\n const [length, setLength] = useState(0);\r\n const withCounter = maxLength !== undefined && counter;\r\n const isInvalid = Boolean(error) || Boolean(field?.invalid);\r\n\r\n const setRef = useCallback(\r\n (node: HTMLTextAreaElement | null) => {\r\n textareaRef.current = node;\r\n assignRef(ref, node);\r\n },\r\n [ref],\r\n );\r\n\r\n /**\r\n * ponytail: длину счётчик читает из DOM — одинаково для `value` и `defaultValue`,\r\n * включая автозаполнение. Потолок: reset формы в неконтролируемом режиме счётчик\r\n * не слышит и догоняет на следующем вводе; апгрейд — слушать `reset`, как `Input`.\r\n * Тот же проход отдаёт показанную строку в Field.\r\n */\r\n useEffect(() => {\r\n const node = textareaRef.current;\r\n if (!node) return;\r\n\r\n if (withCounter) setLength(node.value.length);\r\n reportValue?.(node.value);\r\n }, [reportValue, value, withCounter]);\r\n\r\n const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {\r\n if (withCounter) setLength(event.target.value.length);\r\n if (value === undefined) reportValue?.(event.target.value);\r\n onChange?.(event);\r\n };\r\n\r\n const counterId = withCounter ? `${textareaId}-counter` : undefined;\r\n const describedBy = [field?.describedBy, counterId, ariaDescribedBy]\r\n .filter(part => Boolean(part))\r\n .join(' ');\r\n\r\n return (\r\n <div className=\"textarea__wrap\">\r\n <textarea\r\n {...props}\r\n ref={setRef}\r\n id={textareaId}\r\n className={cn(\r\n 'textarea',\r\n className,\r\n when(size !== 'md', `textarea--${size}`),\r\n when(recommended, 'textarea--recommended'),\r\n when(isInvalid, 'textarea--error'),\r\n )}\r\n {...(value !== undefined ? { value } : {})}\r\n maxLength={maxLength}\r\n onChange={handleChange}\r\n {...(isInvalid ? { 'aria-invalid': true } : {})}\r\n {...(field?.required\r\n ? { 'required': true, 'aria-required': true }\r\n : {})}\r\n {...(describedBy ? { 'aria-describedby': describedBy } : {})}\r\n {...(field?.errorId ? { 'aria-errormessage': field.errorId } : {})}\r\n />\r\n {withCounter ? (\r\n <span id={counterId} className=\"textarea__counter\">\r\n Осталось {Math.max(0, (maxLength ?? 0) - length)} из {maxLength}{' '}\r\n символов\r\n </span>\r\n ) : null}\r\n </div>\r\n );\r\n },\r\n);\r\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,WAAW,WACtB,SAAS,SACP,EACE,WACA,OACA,OAAO,MACP,aACA,OACA,UAAU,MACV,WACA,IACA,UACA,oBAAoB,iBACpB,GAAG,SAEL,KACA;CACA,MAAM,SAAS,MAAM;CACrB,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,cAAc,OAAO;CAC3B,MAAM,aAAa,OAAO,MAAM,MAAM;CACtC,MAAM,cAAc,OAA4B,IAAI;CACpD,MAAM,CAAC,QAAQ,aAAa,SAAS,CAAC;CACtC,MAAM,cAAc,cAAc,KAAA,KAAa;CAC/C,MAAM,YAAY,QAAQ,KAAK,KAAK,QAAQ,OAAO,OAAO;CAE1D,MAAM,SAAS,aACZ,SAAqC;EACpC,YAAY,UAAU;EACtB,UAAU,KAAK,IAAI;CACrB,GACA,CAAC,GAAG,CACN;;;;;;;CAQA,gBAAgB;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,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,YAAD;GACE,GAAI;GACJ,KAAK;GACL,IAAI;GACJ,WAAW,GACT,YACA,WACA,KAAK,SAAS,MAAM,aAAa,MAAM,GACvC,KAAK,aAAa,uBAAuB,GACzC,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,qBAAC,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,85 +1,85 @@
|
|
|
1
|
-
@use '../../styles/mixins.scss' as mixins;
|
|
2
|
-
|
|
3
|
-
.textarea__wrap {
|
|
4
|
-
position: relative;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.textarea {
|
|
8
|
-
// Паддинги как у Input соответствующего размера, высота — 0.75 / 1 / 1.25 от md.
|
|
9
|
-
--textarea-padding-y: var(--space-2);
|
|
10
|
-
--textarea-padding-x: var(--space-3);
|
|
11
|
-
--textarea-min-height: 160px;
|
|
12
|
-
box-sizing: border-box;
|
|
13
|
-
width: 100%;
|
|
14
|
-
min-height: var(--textarea-min-height);
|
|
15
|
-
padding: var(--textarea-padding-y) var(--textarea-padding-x);
|
|
16
|
-
font-size: var(--text-sm);
|
|
17
|
-
line-height: var(--text-sm--line-height);
|
|
18
|
-
background-color: transparent;
|
|
19
|
-
color: var(--color-foreground);
|
|
20
|
-
border-radius: var(--radius-sm);
|
|
21
|
-
resize: vertical;
|
|
22
|
-
font-family: var(--font-sans);
|
|
23
|
-
border: 1px solid var(--color-input);
|
|
24
|
-
outline: var(--focus-ring-width) solid transparent;
|
|
25
|
-
transition:
|
|
26
|
-
background 0.3s,
|
|
27
|
-
border-color 0.3s,
|
|
28
|
-
box-shadow 0.2s;
|
|
29
|
-
|
|
30
|
-
&:focus {
|
|
31
|
-
border-color: var(--color-ring);
|
|
32
|
-
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
&:disabled {
|
|
36
|
-
background-color: var(--color-muted);
|
|
37
|
-
border-color: var(--color-muted);
|
|
38
|
-
color: var(--color-muted-foreground);
|
|
39
|
-
fill: var(--color-muted-foreground);
|
|
40
|
-
resize: none;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@include mixins.min-width(md) {
|
|
44
|
-
--textarea-min-height: 80px;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Медиазапрос базы стоит выше и при равной специфичности проиграет, поэтому у
|
|
48
|
-
// каждого размера своя пара значений.
|
|
49
|
-
&--sm {
|
|
50
|
-
--textarea-padding-y: 4px;
|
|
51
|
-
--textarea-padding-x: 12px;
|
|
52
|
-
--textarea-min-height: 120px;
|
|
53
|
-
|
|
54
|
-
@include mixins.min-width(md) {
|
|
55
|
-
--textarea-min-height: 60px;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
&--lg {
|
|
60
|
-
--textarea-padding-y: var(--space-3);
|
|
61
|
-
--textarea-padding-x: 24px;
|
|
62
|
-
--textarea-min-height: 200px;
|
|
63
|
-
|
|
64
|
-
@include mixins.min-width(md) {
|
|
65
|
-
--textarea-min-height: 100px;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
&--recommended {
|
|
70
|
-
background-color: var(--color-recommended);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
&--error {
|
|
74
|
-
border-color: color-mix(in srgb, var(--color-destructive) 50%, transparent);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.textarea__counter {
|
|
79
|
-
display: block;
|
|
80
|
-
margin-top: var(--space-1);
|
|
81
|
-
font-size: var(--text-xs);
|
|
82
|
-
line-height: var(--text-sm--line-height);
|
|
83
|
-
color: var(--color-muted-foreground);
|
|
84
|
-
text-align: left;
|
|
85
|
-
}
|
|
1
|
+
@use '../../styles/mixins.scss' as mixins;
|
|
2
|
+
|
|
3
|
+
.textarea__wrap {
|
|
4
|
+
position: relative;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.textarea {
|
|
8
|
+
// Паддинги как у Input соответствующего размера, высота — 0.75 / 1 / 1.25 от md.
|
|
9
|
+
--textarea-padding-y: var(--space-2);
|
|
10
|
+
--textarea-padding-x: var(--space-3);
|
|
11
|
+
--textarea-min-height: 160px;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
width: 100%;
|
|
14
|
+
min-height: var(--textarea-min-height);
|
|
15
|
+
padding: var(--textarea-padding-y) var(--textarea-padding-x);
|
|
16
|
+
font-size: var(--text-sm);
|
|
17
|
+
line-height: var(--text-sm--line-height);
|
|
18
|
+
background-color: transparent;
|
|
19
|
+
color: var(--color-foreground);
|
|
20
|
+
border-radius: var(--radius-sm);
|
|
21
|
+
resize: vertical;
|
|
22
|
+
font-family: var(--font-sans);
|
|
23
|
+
border: 1px solid var(--color-input);
|
|
24
|
+
outline: var(--focus-ring-width) solid transparent;
|
|
25
|
+
transition:
|
|
26
|
+
background 0.3s,
|
|
27
|
+
border-color 0.3s,
|
|
28
|
+
box-shadow 0.2s;
|
|
29
|
+
|
|
30
|
+
&:focus {
|
|
31
|
+
border-color: var(--color-ring);
|
|
32
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&:disabled {
|
|
36
|
+
background-color: var(--color-muted);
|
|
37
|
+
border-color: var(--color-muted);
|
|
38
|
+
color: var(--color-muted-foreground);
|
|
39
|
+
fill: var(--color-muted-foreground);
|
|
40
|
+
resize: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@include mixins.min-width(md) {
|
|
44
|
+
--textarea-min-height: 80px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Медиазапрос базы стоит выше и при равной специфичности проиграет, поэтому у
|
|
48
|
+
// каждого размера своя пара значений.
|
|
49
|
+
&--sm {
|
|
50
|
+
--textarea-padding-y: 4px;
|
|
51
|
+
--textarea-padding-x: 12px;
|
|
52
|
+
--textarea-min-height: 120px;
|
|
53
|
+
|
|
54
|
+
@include mixins.min-width(md) {
|
|
55
|
+
--textarea-min-height: 60px;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&--lg {
|
|
60
|
+
--textarea-padding-y: var(--space-3);
|
|
61
|
+
--textarea-padding-x: 24px;
|
|
62
|
+
--textarea-min-height: 200px;
|
|
63
|
+
|
|
64
|
+
@include mixins.min-width(md) {
|
|
65
|
+
--textarea-min-height: 100px;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&--recommended {
|
|
70
|
+
background-color: var(--color-recommended);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&--error {
|
|
74
|
+
border-color: color-mix(in srgb, var(--color-destructive) 50%, transparent);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.textarea__counter {
|
|
79
|
+
display: block;
|
|
80
|
+
margin-top: var(--space-1);
|
|
81
|
+
font-size: var(--text-xs);
|
|
82
|
+
line-height: var(--text-sm--line-height);
|
|
83
|
+
color: var(--color-muted-foreground);
|
|
84
|
+
text-align: left;
|
|
85
|
+
}
|