@cyber-harbour/ui 1.0.55 → 1.0.57

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.55",
3
+ "version": "1.0.57",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,5 +1,5 @@
1
1
  import { ButtonSize, getButtonSizeStyles } from '../../Theme';
2
- import { useRef } from 'react';
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Popover, PopoverAlign, PopoverPosition } from 'react-tiny-popover';
4
4
  import { styled, useTheme } from 'styled-components';
5
5
  import { ChevronDownIcon, ChevronUpIcon } from '../IconComponents';
@@ -18,6 +18,7 @@ interface ContextMenuProps {
18
18
  align?: PopoverAlign;
19
19
  hasBorder?: boolean;
20
20
  maxHeight?: number;
21
+ matchAnchorWidth?: boolean;
21
22
  }
22
23
 
23
24
  export const ContextMenu = ({
@@ -34,10 +35,17 @@ export const ContextMenu = ({
34
35
  children,
35
36
  hasBorder = true,
36
37
  maxHeight = 500,
38
+ matchAnchorWidth = false,
37
39
  }: ContextMenuProps) => {
40
+ const [anchorWidth, setAnchorWidth] = useState<number>(0);
38
41
  const buttonRef = useRef<HTMLButtonElement | null>(null);
39
42
 
40
43
  const theme = useTheme();
44
+ useLayoutEffect(() => {
45
+ if (buttonRef.current) {
46
+ setAnchorWidth(buttonRef.current.offsetWidth);
47
+ }
48
+ }, [matchAnchorWidth]);
41
49
 
42
50
  return (
43
51
  <Popover
@@ -47,6 +55,7 @@ export const ContextMenu = ({
47
55
  align={align}
48
56
  onClickOutside={onClickOutside}
49
57
  content={children}
58
+ ref={buttonRef}
50
59
  containerStyle={{
51
60
  backgroundColor: theme.colors.background,
52
61
  border: `1px solid ${theme.colors.stroke.light}`,
@@ -55,10 +64,10 @@ export const ContextMenu = ({
55
64
  overflow: 'auto',
56
65
  maxHeight: `${maxHeight}px`,
57
66
  zIndex: `${9999}`,
67
+ width: anchorWidth && matchAnchorWidth ? `${anchorWidth}px` : undefined,
58
68
  }}
59
69
  >
60
70
  <StyledButton
61
- ref={buttonRef}
62
71
  onClick={onClick}
63
72
  $disabled={disabled}
64
73
  $fullWidth={fullWidth}
@@ -68,7 +77,7 @@ export const ContextMenu = ({
68
77
  disabled={disabled}
69
78
  $hasBorder={hasBorder}
70
79
  >
71
- <div>{anchor}</div>
80
+ <StyledAnchor>{anchor}</StyledAnchor>
72
81
  {isOpen ? (
73
82
  <ChevronUpIcon width={theme.contextMenu.icon.size} height={theme.contextMenu.icon.size} />
74
83
  ) : (
@@ -78,7 +87,10 @@ export const ContextMenu = ({
78
87
  </Popover>
79
88
  );
80
89
  };
81
-
90
+ const StyledAnchor = styled.div`
91
+ flex-grow: 1;
92
+ text-align: start;
93
+ `;
82
94
  // Створюємо стилізований компонент, що використовує уніфіковану палітру
83
95
  const StyledButton = styled.button<{
84
96
  $size: ButtonSize;
@@ -1,17 +1,20 @@
1
1
  import { createPortal } from 'react-dom';
2
2
  import { styled } from 'styled-components';
3
- import { pxToRem } from '../../Theme';
4
- import { CrossIcon } from '../IconComponents';
3
+ import { createComponent, FabricComponent, generatePropertySpaceStyle, propToRem, pxToRem } from '../../Theme';
4
+ import { useBodyScrollLock } from '../../utils';
5
5
  import { useEffect, useRef } from 'react';
6
6
 
7
7
  type DrawerProps = {
8
8
  isOpen: boolean;
9
9
  onClose: () => void;
10
- children: any;
11
- header?: number;
12
- width?: number;
10
+ children?: any;
11
+ header?: number; //TODO: remove deps;
12
+ width?: string | number;
13
13
  };
14
+
14
15
  export const Drawer = (props: DrawerProps) => {
16
+ useBodyScrollLock(props.isOpen);
17
+
15
18
  if (!props.isOpen) return null;
16
19
  return createPortal(<DrawerWithOutclick {...props} />, document.body);
17
20
  };
@@ -30,53 +33,63 @@ const DrawerWithOutclick = ({ onClose, children, width, header }: DrawerProps) =
30
33
  document.removeEventListener('mousedown', handleClick);
31
34
  };
32
35
  }, [onClose]);
36
+
33
37
  return (
34
38
  <StyledDrawer $header={header} $width={width} ref={drawerRef}>
35
- <CloseButton onClick={onClose}>
36
- <CrossIcon width={14} height={14} color="currentColor" />
37
- </CloseButton>
38
- <Content>{children}</Content>
39
+ {children}
39
40
  </StyledDrawer>
40
41
  );
41
42
  };
42
43
 
43
- const CloseButton = styled.button(
44
- ({ theme }) => `
45
- display: block;
46
- position: absolute;
47
- background-color: inherit;
48
- top: ${pxToRem(16, theme.baseSize)};
49
- right: ${pxToRem(16, theme.baseSize)};
50
- cursor: pointer;
51
- z-index: 1;
44
+ type DrawerHeaderProps = FabricComponent<{
45
+ children?: any;
46
+ }>;
52
47
 
53
- color: ${theme.colors.text.light};
54
- padding: ${pxToRem(4, theme.baseSize)};
55
- border: none;
56
- outline: none;
57
- transition: color 0.2s ease;
58
- &: hover {
59
- color: ${theme.colors.primary.light};
60
- }
61
- `
48
+ export const DrawerHeader: any = createComponent<DrawerHeaderProps>(
49
+ styled.div<DrawerHeaderProps>(
50
+ ({ theme, p = theme.drawer.padding }) => `
51
+ grid-area: drawer-header;
52
+ ${generatePropertySpaceStyle(theme, 'padding', p)};
53
+ `
54
+ ),
55
+ {
56
+ ignoreStyles: ['padding-inline'],
57
+ }
62
58
  );
63
59
 
64
- const Content = styled.div(
65
- ({ theme }) => `
66
- max-height: 100%;
67
- overflow-y: auto;
60
+ type DrawerBodyProps = FabricComponent<{
61
+ children?: any;
62
+ }>;
63
+
64
+ export const DrawerBody: any = createComponent(
65
+ styled.div<DrawerBodyProps>(
66
+ ({ theme, px = theme.drawer.padding, pb = theme.drawer.padding }) => `
67
+ grid-area: drawer-body;
68
+ max-height: 100%;
69
+ overflow-y: auto;
70
+ ${generatePropertySpaceStyle(theme, 'padding-inline', px)};
71
+ ${generatePropertySpaceStyle(theme, 'padding-bottom', pb)};
68
72
  `
73
+ ),
74
+ {
75
+ ignoreStyles: ['padding', 'padding-bottom'],
76
+ }
69
77
  );
70
- const StyledDrawer = styled.div<{ $header?: number; $width?: number }>(
78
+
79
+ const StyledDrawer = styled.div<{ $header?: number; $width?: string | number }>(
71
80
  ({ theme, $header, $width }) => `
72
81
  position: fixed;
82
+ display: grid;
83
+ grid-template-areas:
84
+ "drawer-header"
85
+ "drawer-body";
86
+ grid-template-rows: auto 1fr;
73
87
  z-index: ${theme.zIndex.sticky};
74
88
  top: ${pxToRem($header || 0, theme.baseSize)};
75
89
  bottom: 0;
76
90
  right: 0;
77
- width: ${$width ? pxToRem($width, theme.baseSize) : theme.drawer.width};
91
+ width: ${$width ? propToRem($width, theme.baseSize) : theme.drawer.width};
78
92
  max-width: 100%;
79
- padding: ${theme.drawer.padding};
80
93
  background-color: ${theme.colors.background};
81
94
  box-shadow: ${theme.drawer.shadow};
82
95
  border-left: 1px solid ${theme.colors.stroke.lighter};
@@ -0,0 +1,36 @@
1
+ import { SVGProps } from 'react';
2
+
3
+ interface RelationPointsIconProps extends SVGProps<SVGSVGElement> {
4
+ fill?: string;
5
+ }
6
+
7
+ export const RelationPointsIcon = ({ fill = '#80A0F5', ...props }: RelationPointsIconProps) => {
8
+ return (
9
+ <svg viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
10
+ <path
11
+ d="M4.90056 13.6504C5.95056 13.476 5.95056 14.0866 6.65056 14.6099C9.88806 17.0087 14.0443 17.314 17.5443 15.2205C18.0256 14.9152 19.4693 13.6504 19.6881 13.6068C20.8693 13.4324 23.0131 15.0461 23.7568 15.9184C25.1568 17.7065 25.3318 20.4978 25.0693 22.6785C24.9381 23.856 23.9756 24.8155 22.7943 24.9464C16.2756 24.5103 8.92556 25.557 2.53806 24.9464C1.44431 24.8592 0.350563 24.0741 0.175563 22.9402C-0.0869367 21.4573 0.0880633 18.4479 0.744313 17.0523C1.40056 15.6567 3.41306 13.8685 4.94431 13.6068L4.90056 13.6504Z"
12
+ fill={fill}
13
+ />
14
+ <path
15
+ d="M49.9618 58.4401C50.7931 58.3093 51.4931 59.4432 52.2806 59.923C55.3868 61.8856 59.5868 61.8856 62.6931 59.923C63.4806 59.4432 64.1806 58.3093 65.0118 58.4401C66.8493 58.7018 68.9931 60.708 69.6056 62.409C70.0431 63.5865 70.1743 67.1629 69.7368 68.2532C69.4306 69.0383 68.5118 69.6925 67.6806 69.7797C61.1618 69.3436 53.8118 70.3903 47.4243 69.7797C46.3306 69.6925 45.2368 68.9074 45.0618 67.7735C44.8868 66.6395 44.9743 63.7174 45.2806 62.627C45.8493 60.7953 48.0368 58.7018 49.9618 58.4401Z"
16
+ fill={fill}
17
+ />
18
+ <path
19
+ d="M32.1562 3.83814C38.325 3.35839 44.4937 4.53596 49.9187 7.50169C50.4875 7.80699 53.1125 9.68237 53.3312 9.68237C53.4187 9.68237 54.25 9.15901 54.6437 9.02817C58.5812 7.89421 62.125 11.4269 60.9437 15.3522C60.8125 15.7447 60.2875 16.617 60.2875 16.6606C60.2875 16.8786 62.1688 19.4955 62.4313 20.0624C65.8438 26.2992 67.5937 36.5048 65.0562 43.3085C64.4437 44.9659 62.825 44.9659 62.4313 43.3085C62.7375 41.8693 63.1312 40.43 63.35 38.9472C64.1812 32.6232 62.9562 25.9067 59.675 20.4113C59.4125 20.0188 58.5812 18.536 58.1875 18.4923C57.925 18.4923 57.575 18.754 57.225 18.7976C53.4187 19.1465 50.925 16.835 51.2312 12.9534C51.2312 12.43 51.7125 11.9503 51.4062 11.5578C51.1 11.1652 48.3 9.63876 47.6875 9.33347C41.2125 6.14967 33.8625 5.58269 26.95 7.5453C25.8562 7.8506 23.5812 9.02817 22.6625 8.50481C21.7875 7.98144 22.0062 6.76026 22.8812 6.23689C24.85 5.10294 29.8812 4.05621 32.2 3.88175L32.1562 3.83814Z"
20
+ fill={fill}
21
+ />
22
+ <path
23
+ d="M5.3366 27.4741C8.0491 26.8636 6.60535 30.7888 6.4741 32.1844C5.90535 37.9851 7.3491 44.6579 10.4116 49.5863C10.5866 49.8916 11.3303 51.1128 11.5491 51.2436C11.9866 51.5489 12.3366 51.1128 12.8178 51.0692C16.6678 50.7203 19.1178 53.0318 18.8116 56.9134C18.8116 57.3495 18.3741 57.9165 18.5491 58.2218C18.7241 58.5707 21.8303 60.3153 22.4428 60.577C27.0803 62.8013 32.6366 63.848 37.7553 63.3682C39.1553 63.2374 42.1741 61.9726 42.6116 63.848C43.0491 65.7234 38.6741 65.9414 37.4491 66.0287C31.5866 66.3776 25.7241 65.2436 20.5178 62.5396C19.9053 62.1907 16.9303 60.1844 16.7116 60.1844C16.6241 60.1844 16.1866 60.5333 15.9678 60.6206C11.5053 62.3215 7.5241 58.3963 9.2741 53.9477C9.3616 53.7296 9.7116 53.2498 9.7116 53.2062C9.7116 52.9882 7.83035 50.3713 7.56785 49.8044C4.68035 44.5271 3.45535 38.552 3.8491 32.4897C3.89285 31.6611 4.33035 28.3464 4.76785 27.8231C4.8991 27.6486 5.11785 27.5178 5.3366 27.4741Z"
24
+ fill={fill}
25
+ />
26
+ <path
27
+ d="M11.2893 0.0856923C22.3143 -1.39717 22.3143 15.2633 11.7268 14.2165C3.41434 13.3879 3.63309 1.13242 11.2893 0.0856923Z"
28
+ fill={fill}
29
+ />
30
+ <path
31
+ d="M56.3517 44.9207C66.4142 43.6995 67.2892 58.1793 58.3204 59.0079C48.6517 59.9238 47.7767 45.9238 56.3517 44.9207Z"
32
+ fill={fill}
33
+ />
34
+ </svg>
35
+ );
36
+ };
@@ -55,3 +55,4 @@ export { AndroidIcon } from './AndroidIcon';
55
55
  export { MicrosoftIcon } from './MicrosoftIcon';
56
56
  export { FolderAlertIcon } from './FolderAlertIcon';
57
57
  export { RelationIcon } from './RelationIcon';
58
+ export { RelationPointsIcon } from './RelationPointsIcon';
@@ -89,7 +89,7 @@ const TextAreaInput = forwardRef<HTMLTextAreaElement, TextAreaInputProps>(functi
89
89
 
90
90
  return (
91
91
  <div style={{ position: 'relative', width: '100%' }}>
92
- <StyledTextarea disabled={disabled} {...props} rows={areaSize} ref={ref} />
92
+ <StyledTextarea disabled={disabled} $autoResize={autoResize} {...props} rows={areaSize} ref={ref} />
93
93
  <SizeContainer
94
94
  ref={divRef}
95
95
  $size={size}
@@ -111,11 +111,14 @@ type StyledGroupProps = StyledInputProps & {
111
111
  $multiline?: boolean;
112
112
  };
113
113
 
114
- const StyledTextarea = styled.textarea`
114
+ const StyledTextarea = styled.textarea<{ $autoResize: boolean }>(
115
+ ({ $autoResize }) => `
115
116
  resize: none;
116
117
  margin: 0;
117
118
  display: block;
118
- `;
119
+ ${$autoResize ? 'overflow: hidden;' : ''}
120
+ `
121
+ );
119
122
 
120
123
  const StyledInput = styled.input``;
121
124
 
@@ -1,13 +1,13 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useMemo, useState } from 'react';
2
2
  import { PopoverAlign, PopoverPosition } from 'react-tiny-popover';
3
3
  import { ContextMenu } from '../ContextMenu';
4
4
  import { ButtonSize, getButtonSizeStyles } from '../../Theme';
5
5
  import { styled } from 'styled-components';
6
6
 
7
- interface SelectProps<T extends string | number> {
7
+ type SelectBaseProps<T extends string | number> = {
8
8
  selected?: T;
9
9
  options: { value: T; inputDisplay?: string }[];
10
- handleSelect: (id: T) => void;
10
+ onSelect: (id: T) => void;
11
11
  placeholder: string;
12
12
  disabled?: boolean;
13
13
  positions?: PopoverPosition[] | PopoverPosition;
@@ -15,12 +15,25 @@ interface SelectProps<T extends string | number> {
15
15
  size?: ButtonSize;
16
16
  hasBorder?: boolean;
17
17
  maxHeight?: number;
18
- }
18
+ matchAnchorWidth?: boolean;
19
+ };
20
+
21
+ type SelectDefaultProps = {
22
+ isSearchable?: false;
23
+ };
24
+
25
+ type SelectSearchableProps = {
26
+ isSearchable: true;
27
+ noOptionsMessage: string;
28
+ inputPlaceholder: string;
29
+ };
30
+
31
+ type SelectProps<T extends string | number> = SelectBaseProps<T> & (SelectSearchableProps | SelectDefaultProps);
19
32
 
20
33
  export const Select = <T extends string | number>({
21
34
  options,
22
35
  selected,
23
- handleSelect,
36
+ onSelect,
24
37
  placeholder,
25
38
  disabled = false,
26
39
  positions = ['bottom'],
@@ -28,31 +41,68 @@ export const Select = <T extends string | number>({
28
41
  size = 'small',
29
42
  hasBorder = true,
30
43
  maxHeight,
44
+ matchAnchorWidth = true,
45
+ isSearchable = false,
46
+ ...props
31
47
  }: SelectProps<T>) => {
32
48
  const [isOpen, setIsOpen] = useState<boolean>(false);
49
+ const [searchValue, setSearchValue] = useState<string>('');
50
+
51
+ const visibleOptions = useMemo(() => {
52
+ if (!isSearchable) return options;
53
+
54
+ const normalizedValue = searchValue?.trim().toLocaleLowerCase() || '';
55
+ return options.filter(({ value, inputDisplay }) =>
56
+ String(inputDisplay || value)
57
+ .toLocaleLowerCase()
58
+ .includes(normalizedValue)
59
+ );
60
+ }, [options, searchValue, isSearchable]);
61
+
33
62
  const handleToggle = useCallback(() => {
34
63
  if (!disabled) setIsOpen((prev) => !prev);
35
64
  }, []);
36
65
 
66
+ const noOptionsMessage = 'noOptionsMessage' in props ? props.noOptionsMessage : '';
67
+ const inputPlaceholder = 'inputPlaceholder' in props ? props.inputPlaceholder : '';
68
+
37
69
  return (
38
70
  <ContextMenu
39
71
  isOpen={isOpen}
40
72
  onClickOutside={() => setIsOpen(false)}
41
73
  onClick={handleToggle}
42
74
  disabled={disabled}
43
- anchor={!selected ? placeholder : options.find((option) => option.value === selected)?.inputDisplay || selected}
75
+ anchor={
76
+ isSearchable && isOpen ? (
77
+ <StyledSearchInput
78
+ type="text"
79
+ value={searchValue}
80
+ onChange={(e) => setSearchValue(e.target.value)}
81
+ placeholder={inputPlaceholder}
82
+ autoFocus
83
+ onClick={(e) => e.stopPropagation()}
84
+ $size={size}
85
+ />
86
+ ) : !selected ? (
87
+ placeholder
88
+ ) : (
89
+ options.find((option) => option.value === selected)?.inputDisplay || selected
90
+ )
91
+ }
44
92
  fullWidth
45
93
  positions={positions}
46
94
  align={align}
47
95
  size={size}
48
96
  hasBorder={hasBorder}
49
97
  maxHeight={maxHeight}
98
+ matchAnchorWidth={matchAnchorWidth}
50
99
  >
51
100
  <StyledWrapper>
52
- {options.map((item) => (
101
+ {visibleOptions.map((item) => (
53
102
  <StyledItem
54
103
  onClick={() => {
55
- handleSelect(item.value);
104
+ onSelect(item.value);
105
+ if (isSearchable) setSearchValue('');
56
106
  setIsOpen(false);
57
107
  }}
58
108
  type="button"
@@ -64,19 +114,34 @@ export const Select = <T extends string | number>({
64
114
  {item.inputDisplay || item.value}
65
115
  </StyledItem>
66
116
  ))}
117
+ {!visibleOptions.length && <NoMatchMessage $size={size}>{noOptionsMessage}</NoMatchMessage>}
67
118
  </StyledWrapper>
68
119
  </ContextMenu>
69
120
  );
70
121
  };
71
122
 
72
- const StyledWrapper = styled.div`
73
- padding-block: 7px;
74
- padding-inline: 5px;
75
- button:not(:last-of-type) {
76
- margin-bottom: 4px;
77
- }
78
- `;
123
+ const StyledSearchInput = styled.input<{ $size: ButtonSize }>(
124
+ ({ theme, $size }) => `
125
+ outline: none;
126
+ border: none;
127
+ padding: 0;
128
+ background: transparent;
129
+ width: 100%;
130
+ font-size: ${theme.button.sizes[$size].fontSize};
131
+ color: ${theme.colors.text.main};
132
+ `
133
+ );
79
134
 
135
+ const NoMatchMessage = styled.p<{ $size: ButtonSize }>(
136
+ ({ theme, $size }) => `
137
+ padding-block: ${theme.select.padding};
138
+ padding-inline: ${theme.button.sizes[$size].paddingInline};
139
+ font-size: ${theme.button.sizes[$size].fontSize};
140
+ color: ${theme.colors.text.main};
141
+ white-space: pre-line;
142
+ word-break: break-word;
143
+ `
144
+ );
80
145
  const StyledItem = styled.button<{ $size: ButtonSize; $selected: boolean }>`
81
146
  ${({ theme, $size, $selected }) => {
82
147
  const sizes = getButtonSizeStyles(theme, $size);
@@ -85,7 +150,7 @@ const StyledItem = styled.button<{ $size: ButtonSize; $selected: boolean }>`
85
150
  color: ${theme.select.item.default.text};
86
151
  font-size: ${sizes.fontSize};
87
152
  gap: ${sizes.gap};
88
- padding-block: ${sizes.paddingBlock};
153
+ padding-block: ${theme.select.padding};
89
154
  padding-inline: ${sizes.paddingInline};
90
155
  border-radius: ${sizes.borderRadius};
91
156
  border-width: ${sizes.borderWidth};
@@ -120,3 +185,13 @@ const StyledItem = styled.button<{ $size: ButtonSize; $selected: boolean }>`
120
185
  `;
121
186
  }}
122
187
  `;
188
+
189
+ const StyledWrapper = styled.div(
190
+ ({ theme }) => `
191
+ padding-block: ${theme.select.paddingBlock};
192
+ padding-inline: ${theme.select.paddingInline};
193
+ ${StyledItem}:not(:last-of-type) {
194
+ margin-bottom: ${theme.select.margin};
195
+ }
196
+ `
197
+ );
@@ -697,6 +697,10 @@ export const darkThemePx: Theme = {
697
697
  boxShadow: 'none',
698
698
  },
699
699
  },
700
+ paddingBlock: 7,
701
+ paddingInline: 5,
702
+ margin: 2,
703
+ padding: 6,
700
704
  },
701
705
  // Компонент RowActionsMenu
702
706
  rowActionsMenu: {
@@ -696,6 +696,10 @@ export const lightThemePx: Theme = {
696
696
  boxShadow: 'none',
697
697
  },
698
698
  },
699
+ paddingBlock: 7,
700
+ paddingInline: 5,
701
+ margin: 2,
702
+ padding: 6,
699
703
  },
700
704
  // Компонент RowActionsMenu
701
705
  rowActionsMenu: {
@@ -211,6 +211,10 @@ export type Theme = {
211
211
  //Select
212
212
  select: {
213
213
  item: Record<ButtonState, ButtonElementStyle>;
214
+ paddingBlock: string | number;
215
+ paddingInline: string | number;
216
+ margin: string | number;
217
+ padding: string | number;
214
218
  };
215
219
  // RowActionsMenu
216
220
  rowActionsMenu: {
@@ -1,14 +1,5 @@
1
1
  import { DefaultTheme } from 'styled-components';
2
- import {
3
- Breakpoint,
4
- ButtonColor,
5
- ButtonSize,
6
- ButtonState,
7
- ButtonVariant,
8
- InputSize,
9
- InputState,
10
- InputVariant,
11
- } from './types';
2
+ import { Breakpoint, ButtonColor, ButtonSize, ButtonState, ButtonVariant, InputState, InputVariant } from './types';
12
3
 
13
4
  /**
14
5
  * Helper function to resolve nested color paths from theme
@@ -96,7 +87,11 @@ const IGNORE_CONVERT_KEYS: Record<string, string[] | boolean> = {
96
87
  * @returns The value in rem units as a string (e.g., "1.25rem")
97
88
  */
98
89
  export const propToRem = (value: number | string, baseSize: number = 16): string => {
99
- if (typeof value === 'string' && value.trim().endsWith('%')) return value; // Return percentage values as-is
90
+ // Check if value ends with units that should not be converted to rem
91
+ if (typeof value === 'string' && /(%|d?vh|d?vw)$/.test(value.trim())) {
92
+ return value; // Return percentage and viewport values as-is
93
+ }
94
+
100
95
  const numericValue = typeof value === 'string' ? parseFloat(value) : value;
101
96
 
102
97
  // Handle invalid values
package/src/utils.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * Hook для блокування скролу body та збереження позиції контенту
5
+ * Додає padding-right замість скролбару щоб уникнути стрибків контенту
6
+ *
7
+ * @param isLocked - чи заблоковано скрол
8
+ */
9
+ export const useBodyScrollLock = (isLocked: boolean) => {
10
+ useEffect(() => {
11
+ if (!isLocked) return;
12
+
13
+ const originalStyle = window.getComputedStyle(document.body);
14
+ const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
15
+
16
+ // Зберігаємо оригінальні стилі
17
+ const originalPaddingRight = originalStyle.paddingRight;
18
+ const originalOverflow = originalStyle.overflow;
19
+
20
+ // Блокуємо скрол та додаємо падінг замість скролбару
21
+ document.body.style.overflow = 'hidden';
22
+ document.body.style.paddingRight = `${parseInt(originalPaddingRight) + scrollBarWidth}px`;
23
+
24
+ return () => {
25
+ // Відновлюємо оригінальні стилі
26
+ document.body.style.overflow = originalOverflow;
27
+ document.body.style.paddingRight = originalPaddingRight;
28
+ };
29
+ }, [isLocked]);
30
+ };