@cyber-harbour/ui 1.0.44 → 1.0.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-harbour/ui",
3
- "version": "1.0.44",
3
+ "version": "1.0.45",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,9 +1,9 @@
1
- import { InputSize, InputSizeStyle, InputVariant, getInputStyles } from '../../Theme';
2
- import { forwardRef, InputHTMLAttributes, Ref } from 'react';
3
- import { styled } from 'styled-components';
1
+ import { InputSize, InputVariant, remToPx } from '../../Theme';
2
+ import { forwardRef, InputHTMLAttributes, Ref, TextareaHTMLAttributes, useEffect, useRef, useState } from 'react';
3
+ import { styled, useTheme } from 'styled-components';
4
4
  import { InfoCircleFilledIcon } from '../IconComponents';
5
5
 
6
- export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> & {
6
+ type BaseInputProps = {
7
7
  error?: boolean;
8
8
  append?: any;
9
9
  prepend?: any;
@@ -11,15 +11,49 @@ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> & {
11
11
  variant?: InputVariant;
12
12
  };
13
13
 
14
- export const Input: any = forwardRef<HTMLInputElement, InputProps>(function Input(
15
- { error, append, prepend, size = 'small', variant = 'outlined', disabled, className, ...props },
14
+ export type InputElementProps = BaseInputProps &
15
+ InputHTMLAttributes<HTMLInputElement> & {
16
+ multiline?: false;
17
+ };
18
+ export type TextAreaElementProps = BaseInputProps &
19
+ TextareaHTMLAttributes<HTMLTextAreaElement> & {
20
+ multiline: true;
21
+ autoResize?: boolean;
22
+ };
23
+ export type InputProps = InputElementProps | TextAreaElementProps;
24
+
25
+ type TextAreaInputProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
26
+ size?: InputSize;
27
+ autoResize?: boolean;
28
+ };
29
+
30
+ type InputRefElement<T> = T extends true ? HTMLTextAreaElement : HTMLInputElement | null;
31
+
32
+ export const Input: any = forwardRef<InputRefElement<InputProps['multiline']>, InputProps>(function Input(
33
+ { error, append, prepend, size = 'small', variant = 'outlined', multiline, disabled, className, ...props },
16
34
  ref
17
35
  ) {
18
36
  return (
19
- <Group className={className} $error={error} $size={size} $variant={variant} $disabled={!!disabled}>
37
+ <Group
38
+ $align={multiline ? 'flex-start' : 'center'}
39
+ className={className}
40
+ $error={error}
41
+ $size={size}
42
+ $variant={variant}
43
+ $disabled={!!disabled}
44
+ >
20
45
  {!!prepend && prepend}
21
46
  <InputGroup $size={size} $variant={variant}>
22
- <input ref={ref} disabled={disabled} {...props} />
47
+ {multiline ? (
48
+ <TextAreaInput
49
+ size={size}
50
+ disabled={disabled}
51
+ {...(props as TextAreaElementProps)}
52
+ ref={ref as Ref<InputRefElement<true>>}
53
+ />
54
+ ) : (
55
+ <input disabled={disabled} {...(props as InputElementProps)} ref={ref as Ref<InputRefElement<false>>} />
56
+ )}
23
57
  {!!error && (
24
58
  <IconWrapper $variant={variant}>
25
59
  <InfoCircleFilledIcon />
@@ -31,6 +65,52 @@ export const Input: any = forwardRef<HTMLInputElement, InputProps>(function Inpu
31
65
  );
32
66
  });
33
67
 
68
+ const TextAreaInput = forwardRef<HTMLTextAreaElement, TextAreaInputProps>(function Input(
69
+ { size = 'small', disabled, className, rows = '1', autoResize = false, ...props },
70
+ ref
71
+ ) {
72
+ const [areaSize, setAreaSize] = useState(Number(rows));
73
+ const rowsRef = useRef(Number(rows));
74
+ const divRef = useRef<HTMLDivElement>(null);
75
+ const theme = useTheme();
76
+ const rowHeight = useRef(remToPx(theme.input.sizes[size].lineHeight, theme.baseSize) || theme.baseSize);
77
+
78
+ useEffect(() => {
79
+ if (divRef.current && autoResize) {
80
+ const height = divRef.current.getBoundingClientRect().height;
81
+ const areaSize = Math.round(height / rowHeight.current);
82
+ if (rowsRef.current !== areaSize) {
83
+ rowsRef.current = areaSize;
84
+ setAreaSize(areaSize);
85
+ }
86
+ }
87
+ }, [props.value]);
88
+
89
+ return (
90
+ <div style={{ position: 'relative', width: '100%' }}>
91
+ <textarea disabled={disabled} {...props} rows={areaSize} ref={ref} />
92
+ <div
93
+ ref={divRef}
94
+ style={{
95
+ position: 'absolute',
96
+ top: 0,
97
+ left: 0,
98
+ right: 0,
99
+ height: 'auto',
100
+ opacity: 0,
101
+ zIndex: -1,
102
+ pointerEvents: 'none',
103
+ minHeight: rowHeight.current * Number(rows),
104
+ wordBreak: 'break-word',
105
+ whiteSpace: 'pre-wrap',
106
+ }}
107
+ >
108
+ {props.value}
109
+ </div>
110
+ </div>
111
+ );
112
+ });
113
+
34
114
  const InputGroup = styled.div<{ $size: InputSize; $variant?: InputVariant }>(
35
115
  ({ theme, $size, $variant = 'outlined' }) => `
36
116
  display: inline-flex;
@@ -43,7 +123,7 @@ const InputGroup = styled.div<{ $size: InputSize; $variant?: InputVariant }>(
43
123
  height: ${theme.input.sizes[$size].iconSize};
44
124
  }
45
125
 
46
- & input {
126
+ & input, & textarea, & ${EditableContainer} {
47
127
  font-size: ${theme.input.sizes[$size].fontSize};
48
128
  color: inherit;
49
129
  background: transparent;
@@ -63,6 +143,16 @@ const InputGroup = styled.div<{ $size: InputSize; $variant?: InputVariant }>(
63
143
  cursor: not-allowed;
64
144
  color: inherit;
65
145
  }
146
+
147
+ &:focus, &:focus-visible, &:focus:focus-visible {
148
+ outline: none;
149
+ }
150
+ }
151
+
152
+ & textarea {
153
+ resize: none;
154
+ margin: 0;
155
+ display: block;
66
156
  }
67
157
  `
68
158
  );
@@ -74,14 +164,20 @@ const IconWrapper = styled.span<{ $variant: InputVariant }>(
74
164
  align-items: center;
75
165
  color: ${theme.input[$variant].error.icon};
76
166
  margin-right: 10px;
77
-
167
+
78
168
  `
79
169
  );
80
170
 
81
- const Group = styled.div<{ $disabled: boolean; $error?: boolean; $size: InputSize; $variant: InputVariant }>(
82
- ({ theme, $disabled, $error, $size, $variant }) => `
171
+ const Group = styled.div<{
172
+ $align: 'flex-start' | 'center';
173
+ $disabled: boolean;
174
+ $error?: boolean;
175
+ $size: InputSize;
176
+ $variant: InputVariant;
177
+ }>(
178
+ ({ theme, $align = 'center', $disabled, $error, $size, $variant }) => `
83
179
  display: inline-flex;
84
- align-items: center;
180
+ align-items: ${$align};
85
181
  width: 100%;
86
182
  border: 1px solid;
87
183
  border-radius: ${theme.input.sizes[$size].borderRadius};
@@ -110,7 +206,7 @@ const Group = styled.div<{ $disabled: boolean; $error?: boolean; $size: InputSiz
110
206
  &:hover {
111
207
  border-color: ${theme.input[$variant].focus.border};
112
208
  }
113
-
209
+
114
210
  &:focus-within {
115
211
  border-color: ${theme.input[$variant].focus.border};
116
212
  color: ${theme.input[$variant].focus.text};
@@ -130,3 +226,27 @@ const Group = styled.div<{ $disabled: boolean; $error?: boolean; $size: InputSiz
130
226
  }
131
227
  `
132
228
  );
229
+
230
+ const EditableContainer = styled.div<{ $placeholder?: string }>(
231
+ ({ $placeholder, theme }) => `
232
+ outline-style: none;
233
+ outline-width: 0px;
234
+ position: relative;
235
+
236
+ ${
237
+ $placeholder
238
+ ? `
239
+ &:after {
240
+ content: '${$placeholder}';
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ color: ${theme.input.outlined.default.placeholder};
245
+ }
246
+ `
247
+ : ''
248
+ }
249
+
250
+
251
+ `
252
+ );
@@ -579,6 +579,7 @@ export const darkThemePx: Theme = {
579
579
  borderRadius: 0,
580
580
  iconSize: 14,
581
581
  height: 'auto',
582
+ lineHeight: 16,
582
583
  },
583
584
  small: {
584
585
  fontSize: 14,
@@ -587,6 +588,7 @@ export const darkThemePx: Theme = {
587
588
  borderRadius: 5,
588
589
  iconSize: 14,
589
590
  height: 40,
591
+ lineHeight: 16,
590
592
  },
591
593
  medium: {
592
594
  fontSize: 16,
@@ -595,6 +597,7 @@ export const darkThemePx: Theme = {
595
597
  borderRadius: 5,
596
598
  iconSize: 16,
597
599
  height: 40,
600
+ lineHeight: 18,
598
601
  },
599
602
  },
600
603
  outlined: {
@@ -578,6 +578,7 @@ export const lightThemePx: Theme = {
578
578
  borderRadius: 0,
579
579
  iconSize: 14,
580
580
  height: 'auto',
581
+ lineHeight: 16,
581
582
  },
582
583
  small: {
583
584
  fontSize: 14,
@@ -586,6 +587,7 @@ export const lightThemePx: Theme = {
586
587
  borderRadius: 5,
587
588
  iconSize: 14,
588
589
  height: 40,
590
+ lineHeight: 16,
589
591
  },
590
592
  medium: {
591
593
  fontSize: 16,
@@ -594,6 +596,7 @@ export const lightThemePx: Theme = {
594
596
  borderRadius: 5,
595
597
  iconSize: 16,
596
598
  height: 40,
599
+ lineHeight: 18,
597
600
  },
598
601
  },
599
602
  outlined: {
@@ -54,6 +54,7 @@ export type InputSizeStyle = {
54
54
  borderRadius: number | string;
55
55
  iconSize: number | string;
56
56
  height: number | string;
57
+ lineHeight: number;
57
58
  };
58
59
 
59
60
  // Тип для палітри
@@ -73,6 +73,16 @@ export const pxToRem = (pxValue: number | string, baseSize: number = 16): string
73
73
  return `${remValue}rem`;
74
74
  };
75
75
 
76
+ export const remToPx = (remValue: number | string, baseSize: number = 16): number => {
77
+ // If remValue is a string with 'rem' suffix, extract the numeric value
78
+ const numericValue = typeof remValue === 'string' ? parseFloat(remValue.replace('rem', '')) : remValue;
79
+ if (isNaN(numericValue)) {
80
+ return 0;
81
+ }
82
+
83
+ return numericValue * baseSize;
84
+ };
85
+
76
86
  const IGNORE_CONVERT_KEYS: Record<string, string[] | boolean> = {
77
87
  contextMenu: ['padding'],
78
88
  baseSize: true,