@koslibs/components 1.5.0 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @koslibs/components
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#7](https://github.com/koslibs/components/pull/7) [`4e937d7`](https://github.com/koslibs/components/commit/4e937d7e18b3c7d592e3b8cb161817d482bf2c55) Thanks [@holypower777](https://github.com/holypower777)! - Добавлен Textarea по документации Figma: primary/secondary, inner/outer label,
8
+ Typography, hint/error, счётчик длины, очистка, autosize и вертикальный resize.
9
+ Внутренний скроллбар оформлен без стрелок и фона, с тонким закруглённым ползунком.
10
+
3
11
  ## 1.5.0
4
12
 
5
13
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ import { type RefObject } from 'react';
2
+ export declare function useAutosize(ref: RefObject<HTMLTextAreaElement>, autosize: boolean, minRows: number, maxRows?: number, formId?: string): void;
@@ -0,0 +1,106 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+ const useBrowserLayoutEffect = "u" < typeof document ? useEffect : useLayoutEffect;
3
+ function useAutosize(ref, autosize, minRows, maxRows, formId) {
4
+ const updateRef = useRef();
5
+ useBrowserLayoutEffect(()=>{
6
+ if (!autosize) return;
7
+ const textarea = ref.current;
8
+ if (!textarea) return;
9
+ const doc = textarea.ownerDocument;
10
+ const view = doc.defaultView;
11
+ if (!view) return;
12
+ const measure = doc.createElement('textarea');
13
+ measure.tabIndex = -1;
14
+ Object.assign(measure.style, {
15
+ position: 'absolute',
16
+ visibility: 'hidden',
17
+ pointerEvents: 'none',
18
+ height: '0',
19
+ minHeight: '0',
20
+ maxHeight: 'none',
21
+ padding: '0',
22
+ border: '0',
23
+ boxSizing: 'content-box',
24
+ overflow: 'hidden',
25
+ top: '0',
26
+ left: '0'
27
+ });
28
+ doc.body.append(measure);
29
+ const update = ()=>{
30
+ const computed = view.getComputedStyle(textarea);
31
+ const width = textarea.clientWidth;
32
+ if (!width) return;
33
+ Object.assign(measure.style, {
34
+ width: `${width}px`,
35
+ fontFamily: computed.fontFamily,
36
+ fontSize: computed.fontSize,
37
+ fontWeight: computed.fontWeight,
38
+ fontStyle: computed.fontStyle,
39
+ lineHeight: computed.lineHeight,
40
+ letterSpacing: computed.letterSpacing,
41
+ textIndent: computed.textIndent,
42
+ textTransform: computed.textTransform,
43
+ wordBreak: computed.wordBreak,
44
+ overflowWrap: computed.overflowWrap
45
+ });
46
+ measure.wrap = textarea.wrap;
47
+ const lineHeight = parseFloat(computed.lineHeight);
48
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) return;
49
+ measure.value = textarea.value || textarea.placeholder || ' ';
50
+ const height = Math.min(Math.max(measure.scrollHeight, minRows * lineHeight), void 0 === maxRows ? 1 / 0 : maxRows * lineHeight);
51
+ textarea.style.height = `${height}px`;
52
+ };
53
+ update();
54
+ updateRef.current = update;
55
+ const handleInput = ()=>{
56
+ queueMicrotask(()=>{
57
+ if (measure.isConnected) update();
58
+ });
59
+ };
60
+ textarea.addEventListener('input', handleInput);
61
+ const handleReset = (event)=>{
62
+ setTimeout(()=>{
63
+ if (measure.isConnected && !event.defaultPrevented) update();
64
+ }, 0);
65
+ };
66
+ const form = textarea.form;
67
+ form?.addEventListener('reset', handleReset);
68
+ let previousWidth = textarea.clientWidth;
69
+ const observer = "u" < typeof ResizeObserver ? void 0 : new ResizeObserver(()=>{
70
+ const width = textarea.clientWidth;
71
+ if (width !== previousWidth) {
72
+ previousWidth = width;
73
+ update();
74
+ }
75
+ });
76
+ observer?.observe(textarea);
77
+ view.addEventListener('resize', update);
78
+ doc.fonts?.addEventListener('loadingdone', update);
79
+ return ()=>{
80
+ observer?.disconnect();
81
+ view.removeEventListener('resize', update);
82
+ textarea.removeEventListener('input', handleInput);
83
+ form?.removeEventListener('reset', handleReset);
84
+ doc.fonts?.removeEventListener('loadingdone', update);
85
+ measure.remove();
86
+ updateRef.current = void 0;
87
+ };
88
+ }, [
89
+ ref,
90
+ autosize,
91
+ minRows,
92
+ maxRows,
93
+ formId
94
+ ]);
95
+ useBrowserLayoutEffect(()=>{
96
+ updateRef.current?.();
97
+ });
98
+ useBrowserLayoutEffect(()=>{
99
+ if (!autosize) ref.current?.style.removeProperty('height');
100
+ }, [
101
+ ref,
102
+ autosize,
103
+ minRows
104
+ ]);
105
+ }
106
+ export { useAutosize };
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './textarea.js';
2
+ export type { TextareaProps } from './types.js';
@@ -0,0 +1 @@
1
+ export { Textarea } from "./textarea.js";
@@ -0,0 +1,14 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-K9LhZN",
4
+ field: "field-GADSqb",
5
+ textarea: "textarea-vCe_J9",
6
+ value: "value-pi9twE",
7
+ header: "header-AqxZRm",
8
+ innerHeader: "innerHeader-N47qW7",
9
+ counter: "counter-XxZoUu",
10
+ label: "label-fhdpHS",
11
+ clear: "clear-BZMCFP",
12
+ message: "message-_mT8Ov"
13
+ };
14
+ export default index_module;
@@ -0,0 +1,280 @@
1
+ @import "../../styles/theme.css";
2
+ @import "./vars.css";
3
+
4
+ .component-K9LhZN {
5
+ --field-background: var(--input-primary-background-color);
6
+ --field-border: var(--input-border-color);
7
+ --field-color: var(--input-color);
8
+ --field-label-color: var(--input-label-color);
9
+ --field-placeholder-color: var(--input-placeholder-color);
10
+ --field-icon-color: var(--input-icon-color);
11
+ gap: var(--input-gap);
12
+ width: var(--textarea-width);
13
+ vertical-align: top;
14
+ min-width: 0;
15
+ max-width: 100%;
16
+ font-family: var(--font-primary);
17
+ color: var(--field-color);
18
+ flex-direction: column;
19
+ display: inline-flex;
20
+
21
+ &[data-block] {
22
+ width: 100%;
23
+ display: flex;
24
+ }
25
+
26
+ &[data-view="secondary"] {
27
+ --field-background: var(--input-secondary-background-color);
28
+ }
29
+
30
+ &[data-read-only] {
31
+ --field-border: var(--input-read-only-border-color);
32
+ }
33
+
34
+ &[data-error] {
35
+ --field-border: var(--input-error-border-color);
36
+ }
37
+
38
+ &[data-disabled] {
39
+ --field-background: var(--input-disabled-background-color);
40
+ --field-border: transparent;
41
+ --field-color: var(--input-disabled-color);
42
+ --field-label-color: var(--input-disabled-color);
43
+ --field-placeholder-color: var(--input-disabled-color);
44
+ --field-icon-color: var(--input-disabled-color);
45
+ }
46
+ }
47
+
48
+ .field-GADSqb {
49
+ box-sizing: border-box;
50
+ align-items: flex-start;
51
+ gap: var(--input-gap);
52
+ width: 100%;
53
+ padding: var(--textarea-padding);
54
+ border-radius: var(--input-border-radius);
55
+ background: var(--field-background);
56
+ box-shadow: inset 0 0 0 1px var(--field-border);
57
+ cursor: text;
58
+ display: flex;
59
+ }
60
+
61
+ @media (hover: hover) {
62
+ .component-K9LhZN:not([data-disabled], [data-read-only], [data-error]) .field-GADSqb:hover {
63
+ box-shadow: inset 0 0 0 1px var(--input-hover-border-color);
64
+ }
65
+ }
66
+
67
+ .component-K9LhZN:not([data-disabled]) .field-GADSqb:focus-within {
68
+ box-shadow: none;
69
+ outline: 2px solid var(--input-focus-border-color);
70
+ outline-offset: 0;
71
+ }
72
+
73
+ .component-K9LhZN[data-error]:not([data-disabled]) .field-GADSqb:focus-within {
74
+ outline-color: var(--input-error-border-color);
75
+ }
76
+
77
+ .component-K9LhZN[data-disabled] .field-GADSqb, .textarea-vCe_J9:disabled {
78
+ cursor: not-allowed;
79
+ }
80
+
81
+ .value-pi9twE {
82
+ flex-direction: column;
83
+ flex: 1;
84
+ min-width: 0;
85
+ display: flex;
86
+ position: relative;
87
+ }
88
+
89
+ .header-AqxZRm {
90
+ align-items: baseline;
91
+ gap: var(--input-gap);
92
+ min-width: 0;
93
+ display: flex;
94
+ }
95
+
96
+ .innerHeader-N47qW7 {
97
+ width: 100%;
98
+ height: 15px;
99
+ position: absolute;
100
+ top: 0;
101
+ left: 0;
102
+ }
103
+
104
+ .component-K9LhZN .counter-XxZoUu {
105
+ color: var(--field-label-color);
106
+ white-space: nowrap;
107
+ text-align: right;
108
+ font-variant-numeric: tabular-nums;
109
+ flex-shrink: 0;
110
+ margin-left: auto;
111
+ line-height: 16px;
112
+ }
113
+
114
+ .innerHeader-N47qW7 .counter-XxZoUu {
115
+ line-height: 15px;
116
+ }
117
+
118
+ .label-fhdpHS {
119
+ min-width: 0;
120
+ color: var(--field-label-color);
121
+ overflow-wrap: anywhere;
122
+ cursor: inherit;
123
+ flex: 1;
124
+ font-size: 13px;
125
+ line-height: 16px;
126
+ display: block;
127
+ }
128
+
129
+ .component-K9LhZN[data-inner-label] .label-fhdpHS {
130
+ white-space: nowrap;
131
+ text-overflow: ellipsis;
132
+ font-size: var(--textarea-font-size);
133
+ line-height: var(--textarea-line-height);
134
+ transition: transform .18s, font-size .18s, line-height .18s;
135
+ overflow: hidden;
136
+ transform: translateY(7.5px);
137
+
138
+ & > span {
139
+ font-size: inherit;
140
+ line-height: inherit;
141
+ }
142
+ }
143
+
144
+ .component-K9LhZN[data-inner-label][data-filled] .label-fhdpHS, .component-K9LhZN[data-inner-label]:not([data-disabled], [data-read-only]) .value-pi9twE:focus-within .label-fhdpHS {
145
+ font-size: 12px;
146
+ line-height: 15px;
147
+ transform: translateY(0);
148
+ }
149
+
150
+ .component-K9LhZN[data-inner-label] .value-pi9twE:has(.textarea-vCe_J9:autofill) .label-fhdpHS {
151
+ font-size: 12px;
152
+ line-height: 15px;
153
+ transform: translateY(0);
154
+ }
155
+
156
+ .component-K9LhZN[data-inner-header] .textarea-vCe_J9 {
157
+ margin-top: 15px;
158
+ }
159
+
160
+ .component-K9LhZN[data-inner-label] .textarea-vCe_J9::placeholder {
161
+ opacity: 0;
162
+ transition: opacity .18s;
163
+ }
164
+
165
+ .component-K9LhZN[data-inner-label]:not([data-disabled], [data-read-only]) .textarea-vCe_J9:focus::placeholder {
166
+ opacity: 1;
167
+ }
168
+
169
+ .textarea-vCe_J9 {
170
+ box-sizing: border-box;
171
+ width: 100%;
172
+ min-width: 0;
173
+ height: calc(var(--textarea-rows) * var(--textarea-line-height));
174
+ min-height: calc(var(--textarea-rows) * var(--textarea-line-height));
175
+ color: var(--field-color);
176
+ font-family: var(--font-primary);
177
+ font-size: var(--textarea-font-size);
178
+ font-weight: 400;
179
+ line-height: var(--textarea-line-height);
180
+ overflow-wrap: break-word;
181
+ resize: none;
182
+ appearance: none;
183
+ scrollbar-color: auto;
184
+ scrollbar-width: auto;
185
+ background: none;
186
+ border: 0;
187
+ border-radius: 0;
188
+ outline: none;
189
+ margin: 0;
190
+ padding: 0;
191
+ display: block;
192
+ overflow: auto;
193
+
194
+ &::-webkit-scrollbar {
195
+ background: none;
196
+ width: 6px;
197
+ height: 6px;
198
+ }
199
+
200
+ &::-webkit-scrollbar-button {
201
+ width: 0;
202
+ height: 0;
203
+ display: none;
204
+ }
205
+
206
+ &::-webkit-scrollbar-track {
207
+ background: none;
208
+ }
209
+
210
+ &::-webkit-scrollbar-track-piece {
211
+ background: none;
212
+ }
213
+
214
+ &::-webkit-scrollbar-corner {
215
+ background: none;
216
+ }
217
+
218
+ &::-webkit-scrollbar-thumb {
219
+ background: var(--field-icon-color);
220
+ border-radius: 999px;
221
+ }
222
+
223
+ &::placeholder {
224
+ color: var(--field-placeholder-color);
225
+ opacity: 1;
226
+ }
227
+
228
+ &:disabled {
229
+ opacity: 1;
230
+ -webkit-text-fill-color: var(--field-color);
231
+ }
232
+ }
233
+
234
+ @supports not selector(::-webkit-scrollbar) {
235
+ .textarea-vCe_J9 {
236
+ scrollbar-width: thin;
237
+ scrollbar-color: var(--field-icon-color) transparent;
238
+ }
239
+ }
240
+
241
+ .component-K9LhZN[data-resize] .textarea-vCe_J9 {
242
+ resize: vertical;
243
+ }
244
+
245
+ .component-K9LhZN .clear-BZMCFP {
246
+ width: 24px;
247
+ min-width: 0;
248
+ height: 24px;
249
+ min-height: 0;
250
+ color: var(--field-icon-color);
251
+ background: none;
252
+ flex: 0 0 24px;
253
+ margin: 0 -4px;
254
+ padding: 0;
255
+
256
+ &:hover, &:active {
257
+ color: var(--field-icon-color);
258
+ }
259
+
260
+ & svg {
261
+ width: var(--input-icon-size);
262
+ height: var(--input-icon-size);
263
+ display: block;
264
+ }
265
+ }
266
+
267
+ .component-K9LhZN .message-_mT8Ov {
268
+ color: var(--input-hint-color);
269
+ overflow-wrap: anywhere;
270
+ line-height: 15px;
271
+ }
272
+
273
+ .component-K9LhZN[data-error] .message-_mT8Ov {
274
+ color: var(--input-error-message-color);
275
+ }
276
+
277
+ .component-K9LhZN[data-disabled] .message-_mT8Ov {
278
+ color: var(--input-disabled-color);
279
+ }
280
+
@@ -0,0 +1,27 @@
1
+ import { type ChangeEvent, type MouseEvent } from 'react';
2
+ export declare const Textarea: import("react").ForwardRefExoticComponent<Omit<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue" | "children" | "onChange" | "maxLength" | "rows"> & {
3
+ value?: string;
4
+ defaultValue?: string;
5
+ maxLength?: number;
6
+ showValueLength?: boolean;
7
+ view?: "primary" | "secondary";
8
+ block?: boolean;
9
+ label?: import("react").ReactNode;
10
+ labelView?: "outer" | "inner";
11
+ hint?: import("react").ReactNode;
12
+ error?: import("react").ReactNode | boolean;
13
+ minRows?: number;
14
+ maxRows?: number;
15
+ autosize?: boolean;
16
+ resize?: boolean;
17
+ clear?: boolean;
18
+ onChange?: (event: ChangeEvent<HTMLTextAreaElement>, payload: {
19
+ value: string;
20
+ }) => void;
21
+ onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
22
+ fieldClassName?: string;
23
+ textareaClassName?: string;
24
+ labelClassName?: string;
25
+ messageClassName?: string;
26
+ dataTestId?: string;
27
+ } & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -0,0 +1,173 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { CrossHeavyMIcon } from "@koslibs/icons/CrossHeavyMIcon";
3
+ import classnames from "classnames";
4
+ import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
5
+ import { useDataTestId } from "../../hooks/use-data-test-id.js";
6
+ import { Button } from "../Button/index.js";
7
+ import { TypographyText } from "../Typography/text.js";
8
+ import { useAutosize } from "./hooks/use-autosize.js";
9
+ import index_module from "./index.module.js";
10
+ const Textarea = /*#__PURE__*/ forwardRef(({ view = 'primary', block = false, label, labelView = 'outer', hint, error, minRows = 3, maxRows, autosize = false, resize = false, clear = false, value, defaultValue, maxLength, showValueLength = false, onChange, onClear, disabled = false, readOnly = false, id, className, style, fieldClassName, textareaClassName, labelClassName, messageClassName, dataTestId, ...restProps }, ref)=>{
11
+ const generatedId = useId();
12
+ const textareaId = id ?? generatedId;
13
+ const textareaRef = useRef(null);
14
+ const [uncontrolledLength, setUncontrolledLength] = useState(defaultValue?.length ?? 0);
15
+ const controlled = void 0 !== value;
16
+ const valueLength = controlled ? value.length : uncontrolledLength;
17
+ const hasValue = valueLength > 0;
18
+ const showCounter = showValueLength && void 0 !== maxLength && Number.isInteger(maxLength) && maxLength >= 0;
19
+ const hasLabel = null != label && false !== label && '' !== label;
20
+ const errorMessage = hint && 'boolean' != typeof error ? error : void 0;
21
+ const message = errorMessage || hint;
22
+ const showClear = clear && hasValue && !disabled && !readOnly;
23
+ const rows = Number.isFinite(minRows) ? Math.max(1, Math.floor(minRows)) : 3;
24
+ const limit = void 0 !== maxRows && Number.isFinite(maxRows) ? Math.max(rows, Math.floor(maxRows)) : void 0;
25
+ const getDataTestId = useDataTestId(dataTestId);
26
+ useImperativeHandle(ref, ()=>textareaRef.current, []);
27
+ useAutosize(textareaRef, autosize, rows, limit, restProps.form);
28
+ useEffect(()=>{
29
+ const textarea = textareaRef.current;
30
+ const form = textarea?.form;
31
+ let active = true;
32
+ const handleReset = (event)=>{
33
+ setTimeout(()=>{
34
+ if (active && !event.defaultPrevented) setUncontrolledLength(textarea?.value.length ?? 0);
35
+ }, 0);
36
+ };
37
+ form?.addEventListener('reset', handleReset);
38
+ return ()=>{
39
+ active = false;
40
+ form?.removeEventListener('reset', handleReset);
41
+ };
42
+ }, [
43
+ restProps.form
44
+ ]);
45
+ const handleChange = (event)=>{
46
+ const nextValue = event.target.value;
47
+ if (!controlled) setUncontrolledLength(nextValue.length);
48
+ onChange?.(event, {
49
+ value: nextValue
50
+ });
51
+ };
52
+ const handleClear = (event)=>{
53
+ const textarea = textareaRef.current;
54
+ if (!textarea || !showClear) return;
55
+ if (!controlled) {
56
+ textarea.value = '';
57
+ setUncontrolledLength(0);
58
+ }
59
+ onClear?.(event);
60
+ textarea.focus();
61
+ };
62
+ const handleFieldClick = (event)=>{
63
+ if (disabled || event.defaultPrevented || !(event.target instanceof Element)) return;
64
+ const target = event.target.closest('textarea, button, a, label, input, select, [contenteditable], [tabindex]');
65
+ if (target && event.currentTarget.contains(target)) return;
66
+ textareaRef.current?.focus();
67
+ };
68
+ const labelElement = hasLabel && /*#__PURE__*/ jsx("label", {
69
+ htmlFor: textareaId,
70
+ className: classnames(index_module.label, labelClassName),
71
+ "data-test-id": getDataTestId('label'),
72
+ children: /*#__PURE__*/ jsx(TypographyText, {
73
+ size: 'inner' === labelView ? 12 : 13,
74
+ weight: 'inner' === labelView ? 'regular' : 'medium',
75
+ children: label
76
+ })
77
+ });
78
+ const counterElement = showCounter && /*#__PURE__*/ jsxs(TypographyText, {
79
+ size: 11,
80
+ className: index_module.counter,
81
+ style: {
82
+ minWidth: `${2 * String(maxLength).length + 1}ch`
83
+ },
84
+ dataTestId: getDataTestId('counter'),
85
+ children: [
86
+ valueLength,
87
+ "/",
88
+ maxLength
89
+ ]
90
+ });
91
+ return /*#__PURE__*/ jsxs("div", {
92
+ className: classnames(index_module.component, className),
93
+ style: {
94
+ '--textarea-rows': rows,
95
+ ...style
96
+ },
97
+ "data-view": view,
98
+ "data-block": block || void 0,
99
+ "data-disabled": disabled || void 0,
100
+ "data-read-only": readOnly || void 0,
101
+ "data-error": Boolean(error) || void 0,
102
+ "data-label-view": labelView,
103
+ "data-inner-label": 'inner' === labelView && hasLabel || void 0,
104
+ "data-inner-header": 'inner' === labelView && (hasLabel || showCounter) || void 0,
105
+ "data-filled": hasValue || void 0,
106
+ "data-resize": resize && !autosize && !disabled && !readOnly || void 0,
107
+ "data-test-id": getDataTestId('form-control'),
108
+ children: [
109
+ 'outer' === labelView && (hasLabel || showCounter) && /*#__PURE__*/ jsxs("div", {
110
+ className: index_module.header,
111
+ children: [
112
+ labelElement,
113
+ counterElement
114
+ ]
115
+ }),
116
+ /*#__PURE__*/ jsxs("div", {
117
+ className: classnames(index_module.field, fieldClassName),
118
+ onClick: handleFieldClick,
119
+ "data-test-id": getDataTestId('field'),
120
+ children: [
121
+ /*#__PURE__*/ jsxs("div", {
122
+ className: index_module.value,
123
+ children: [
124
+ 'inner' === labelView && (hasLabel || showCounter) && /*#__PURE__*/ jsxs("div", {
125
+ className: classnames(index_module.header, index_module.innerHeader),
126
+ children: [
127
+ labelElement,
128
+ counterElement
129
+ ]
130
+ }),
131
+ /*#__PURE__*/ jsx("textarea", {
132
+ ...restProps,
133
+ ref: textareaRef,
134
+ id: textareaId,
135
+ value: value,
136
+ defaultValue: defaultValue,
137
+ rows: rows,
138
+ maxLength: maxLength,
139
+ disabled: disabled,
140
+ readOnly: readOnly,
141
+ onChange: handleChange,
142
+ className: classnames(index_module.textarea, textareaClassName),
143
+ "data-test-id": dataTestId
144
+ })
145
+ ]
146
+ }),
147
+ showClear && /*#__PURE__*/ jsx(Button, {
148
+ view: "text",
149
+ size: 32,
150
+ className: index_module.clear,
151
+ title: "Очистить поле",
152
+ onMouseDown: (event)=>event.preventDefault(),
153
+ onClick: handleClear,
154
+ dataTestId: getDataTestId('clear'),
155
+ children: /*#__PURE__*/ jsx(CrossHeavyMIcon, {
156
+ width: 16,
157
+ height: 16
158
+ })
159
+ })
160
+ ]
161
+ }),
162
+ Boolean(message) && /*#__PURE__*/ jsx(TypographyText, {
163
+ tag: "div",
164
+ size: 12,
165
+ className: classnames(index_module.message, messageClassName),
166
+ dataTestId: getDataTestId(errorMessage ? 'error' : 'hint'),
167
+ children: message
168
+ })
169
+ ]
170
+ });
171
+ });
172
+ Textarea.displayName = 'Textarea';
173
+ export { Textarea };
@@ -0,0 +1,108 @@
1
+ import type { ChangeEvent, MouseEvent, ReactNode, TextareaHTMLAttributes } from 'react';
2
+ export type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'defaultValue' | 'maxLength' | 'onChange' | 'children' | 'rows'> & {
3
+ /**
4
+ * Значение управляемого поля.
5
+ */
6
+ value?: string;
7
+ /**
8
+ * Начальное значение неуправляемого поля.
9
+ */
10
+ defaultValue?: string;
11
+ /**
12
+ * Максимальная длина нативного ввода в кодовых единицах UTF-16.
13
+ */
14
+ maxLength?: number;
15
+ /**
16
+ * Показывает текущую длину и maxLength рядом с label.
17
+ * Работает только при заданном неотрицательном целом maxLength.
18
+ * @default false
19
+ */
20
+ showValueLength?: boolean;
21
+ /**
22
+ * Вид поля: белый primary или светлый secondary.
23
+ * @default 'primary'
24
+ */
25
+ view?: 'primary' | 'secondary';
26
+ /**
27
+ * Растягивает компонент на ширину контейнера.
28
+ * @default false
29
+ */
30
+ block?: boolean;
31
+ /**
32
+ * Название поля. Клик переводит фокус в textarea.
33
+ */
34
+ label?: ReactNode;
35
+ /**
36
+ * Положение label. Внутренний label поднимается при фокусе или заполнении.
37
+ * @default 'outer'
38
+ */
39
+ labelView?: 'outer' | 'inner';
40
+ /**
41
+ * Подсказка под полем. Сообщение об ошибке заменяет её.
42
+ */
43
+ hint?: ReactNode;
44
+ /**
45
+ * Сообщение об ошибке или true для состояния ошибки без сообщения.
46
+ * Без непустого hint отображается только рамка. true сохраняет hint.
47
+ */
48
+ error?: ReactNode | boolean;
49
+ /**
50
+ * Начальное число видимых строк; при autosize — минимальное число строк.
51
+ * Должно быть положительным целым числом.
52
+ * @default 3
53
+ */
54
+ minRows?: number;
55
+ /**
56
+ * Максимальное число видимых строк при autosize. Затем текст прокручивается.
57
+ * Значение меньше minRows ограничивается minRows.
58
+ */
59
+ maxRows?: number;
60
+ /**
61
+ * Подстраивает высоту под содержимое, включая переносы по ширине.
62
+ * @default false
63
+ */
64
+ autosize?: boolean;
65
+ /**
66
+ * Разрешает менять высоту вручную за нижний правый угол.
67
+ * Не работает вместе с autosize, в disabled и readOnly. Ширина сохраняется.
68
+ * @default false
69
+ */
70
+ resize?: boolean;
71
+ /**
72
+ * Показывает кнопку очистки заполненного редактируемого поля.
73
+ * @default false
74
+ */
75
+ clear?: boolean;
76
+ /**
77
+ * Обработчик ввода с нативным событием React и текущим значением.
78
+ */
79
+ onChange?: (event: ChangeEvent<HTMLTextAreaElement>, payload: {
80
+ value: string;
81
+ }) => void;
82
+ /**
83
+ * Обработчик очистки. Не вызывает onChange.
84
+ * В управляемом режиме обновите value в этом обработчике.
85
+ */
86
+ onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
87
+ /**
88
+ * Класс визуального поля.
89
+ */
90
+ fieldClassName?: string;
91
+ /**
92
+ * Класс нативного textarea. className и style относятся к обёртке.
93
+ */
94
+ textareaClassName?: string;
95
+ /**
96
+ * Класс label.
97
+ */
98
+ labelClassName?: string;
99
+ /**
100
+ * Класс подсказки или сообщения об ошибке.
101
+ */
102
+ messageClassName?: string;
103
+ /**
104
+ * Идентификатор нативного textarea. Части получают суффиксы -form-control,
105
+ * -field, -label, -hint, -error, -clear и -counter.
106
+ */
107
+ dataTestId?: string;
108
+ };
File without changes
@@ -0,0 +1,9 @@
1
+ @import "../Input/vars.css";
2
+
3
+ :root {
4
+ --textarea-width: var(--input-width);
5
+ --textarea-padding: var(--input-padding-40);
6
+ --textarea-font-size: var(--input-font-size-40);
7
+ --textarea-line-height: 17px;
8
+ }
9
+
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { Button } from './components/Button/index.js';
2
2
  export type { ButtonProps } from './components/Button/index.js';
3
3
  export { Input } from './components/Input/index.js';
4
4
  export type { InputProps } from './components/Input/index.js';
5
+ export { Textarea } from './components/Textarea/index.js';
6
+ export type { TextareaProps } from './components/Textarea/index.js';
5
7
  export { Spinner } from './components/Spinner/index.js';
6
8
  export type { SpinnerProps } from './components/Spinner/index.js';
7
9
  export { Typography, TypographyText, TypographyTitle, Text, Title } from './components/Typography/index.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Button } from "./components/Button/index.js";
2
2
  export { Input } from "./components/Input/index.js";
3
+ export { Textarea } from "./components/Textarea/index.js";
3
4
  export { Spinner } from "./components/Spinner/index.js";
4
5
  export { Text, Title, Typography, TypographyText, TypographyTitle } from "./components/Typography/index.js";
5
6
  export { Popover } from "./components/Popover/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koslibs/components",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Shared React components for koslibs projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",