@koslibs/components 1.12.0 → 1.14.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/Button/index_module.css +4 -0
  3. package/dist/components/Input/index_module.css +1 -0
  4. package/dist/components/Modal/components/modal-surface/index.js +1 -1
  5. package/dist/components/Select/index_module.css +3 -2
  6. package/dist/components/Sidepanel/components/content/index.d.ts +13 -0
  7. package/dist/components/Sidepanel/components/content/index.js +21 -0
  8. package/dist/components/Sidepanel/components/content/index.module.js +5 -0
  9. package/dist/components/Sidepanel/components/content/index_module.css +14 -0
  10. package/dist/components/Sidepanel/components/footer/index.d.ts +66 -0
  11. package/dist/components/Sidepanel/components/footer/index.js +25 -0
  12. package/dist/components/Sidepanel/components/footer/index.module.js +19 -0
  13. package/dist/components/Sidepanel/components/footer/index_module.css +57 -0
  14. package/dist/components/Sidepanel/components/header/index.d.ts +33 -0
  15. package/dist/components/Sidepanel/components/header/index.js +45 -0
  16. package/dist/components/Sidepanel/components/header/index.module.js +8 -0
  17. package/dist/components/Sidepanel/components/header/index_module.css +32 -0
  18. package/dist/components/Sidepanel/components/sidepanel-surface/index.d.ts +4 -0
  19. package/dist/components/Sidepanel/components/sidepanel-surface/index.js +92 -0
  20. package/dist/components/Sidepanel/context.d.ts +6 -0
  21. package/dist/components/Sidepanel/context.js +3 -0
  22. package/dist/components/Sidepanel/hooks/use-dialog-transition.d.ts +5 -0
  23. package/dist/components/Sidepanel/hooks/use-dialog-transition.js +83 -0
  24. package/dist/components/Sidepanel/index.d.ts +8 -0
  25. package/dist/components/Sidepanel/index.js +4 -0
  26. package/dist/components/Sidepanel/index.module.js +8 -0
  27. package/dist/components/Sidepanel/index_module.css +68 -0
  28. package/dist/components/Sidepanel/scrollbar.module.js +5 -0
  29. package/dist/components/Sidepanel/scrollbar_module.css +41 -0
  30. package/dist/components/Sidepanel/sidepanel.d.ts +9 -0
  31. package/dist/components/Sidepanel/sidepanel.js +28 -0
  32. package/dist/components/Sidepanel/types.d.ts +36 -0
  33. package/dist/components/Sidepanel/types.js +0 -0
  34. package/dist/components/Sidepanel/vars.css +12 -0
  35. package/dist/components/Tabs/index_module.css +1 -1
  36. package/dist/components/Tabs/tabs.d.ts +0 -1
  37. package/dist/components/Tabs/tabs.js +4 -3
  38. package/dist/components/Tabs/types.d.ts +0 -4
  39. package/dist/components/Textarea/index_module.css +1 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +1 -0
  42. package/dist/utils/lock-scroll.d.ts +1 -0
  43. package/dist/utils/lock-scroll.js +49 -0
  44. package/package.json +1 -1
  45. package/dist/components/Modal/utils/lock-scroll.d.ts +0 -1
  46. package/dist/components/Modal/utils/lock-scroll.js +0 -31
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @koslibs/components
2
2
 
3
+ ## 1.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#16](https://github.com/koslibs/components/pull/16) [`45a76bb`](https://github.com/koslibs/components/commit/45a76bb4b230b95762017468d9497c37d63e2855) Thanks [@holypower777](https://github.com/holypower777)! - Tabs больше не скрывает ряд, когда все вкладки disabled: заголовки и счётчики остаются видимыми без активного индикатора. Удалён проп emptyState: уберите его из вызовов Tabs и отображайте пустое состояние данных в приложении. Без дочерних Tab компонент возвращает null. Добавлены регрессионные тесты и пример в Storybook.
8
+
9
+ ## 1.13.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#15](https://github.com/koslibs/components/pull/15) [`061457d`](https://github.com/koslibs/components/commit/061457d29c63e8fc4ad1929d7c7ca2b9fffae075) Thanks [@holypower777](https://github.com/holypower777)! - Установлен курсор `not-allowed` для disabled-состояний Button и Tab. У Input, Textarea и Select курсор применяется ко всему компоненту, включая внешние подписи и подсказки. У Select disabled имеет приоритет над readOnly.
14
+
15
+ Добавлен Sidepanel: панель шириной 500 px с выездом и уходом справа, бекдропом Modal, Header с необязательным заголовком и hasCloser, прокручиваемым Content и Footer только для Button. Поддержаны строковая и вертикальная раскладки кнопок, управляемое закрытие, удержание и возврат фокуса, Portal и блокировка прокрутки до завершения анимации. Добавлены Storybook, типы, тесты и отдельный импорт.
16
+
17
+ Общая блокировка прокрутки Modal и Sidepanel вынесена в src/utils/lock-scroll.ts. Она сохраняет положение страницы при временном снятии scrollbar-gutter, чтобы панель доходила до края окна.
18
+
19
+ Пустой Header Sidepanel не занимает места. Скроллбар показывает только слайдер без стрелок и фона. Footer поддерживает sticky с дефолтом true; при false кнопки прокручиваются вместе с содержимым. Переключение режима выполнено через классы CSS Modules без служебного data-атрибута.
20
+
21
+ Footer Sidepanel принимает компоненты ComponentType<ButtonProps>: обязательный primary справа и необязательный secondary слева. Внешний вид кнопок задаётся снаружи. gap принимает 12/16/24/32 с дефолтом 12; layout — start/end/center/space-between/column с дефолтом space-between.
22
+
3
23
  ## 1.12.0
4
24
 
5
25
  ### Minor Changes
@@ -21,6 +21,10 @@
21
21
  transition: background-color .1s ease-in-out, color .1s ease-in-out, box-shadow .1s ease-in-out;
22
22
  display: inline-flex;
23
23
  position: relative;
24
+
25
+ &:disabled {
26
+ cursor: not-allowed;
27
+ }
24
28
  }
25
29
 
26
30
  .size-32-oD8MDp {
@@ -42,6 +42,7 @@
42
42
  --field-label-color: var(--input-disabled-color);
43
43
  --field-placeholder-color: var(--input-disabled-color);
44
44
  --field-icon-color: var(--input-disabled-color);
45
+ cursor: not-allowed;
45
46
  }
46
47
  }
47
48
 
@@ -1,8 +1,8 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import classnames from "classnames";
3
3
  import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef } from "react";
4
+ import { lockScroll } from "../../../../utils/lock-scroll.js";
4
5
  import { ModalContext } from "../../context.js";
5
- import { lockScroll } from "../../utils/lock-scroll.js";
6
6
  import colors_module from "../../colors.module.js";
7
7
  import index_module from "../../index.module.js";
8
8
  const ModalSurface = /*#__PURE__*/ forwardRef(({ onClose, size = 720, escapeKeyDown = true, backdropClick = true, lockScroll: shouldLockScroll = true, className, dataTestId, children, onPointerDown, onClick, ...restProps }, ref)=>{
@@ -41,6 +41,7 @@
41
41
  --field-icon-color: var(--select-disabled-color);
42
42
  --field-placeholder-color: var(--select-disabled-color);
43
43
  --field-border: transparent;
44
+ cursor: not-allowed;
44
45
  }
45
46
  }
46
47
 
@@ -204,11 +205,11 @@
204
205
  }
205
206
  }
206
207
 
207
- .component-cM8DTg.disabled-fZIeN_ .trigger-nKdmww {
208
+ .component-cM8DTg.disabled-fZIeN_ .trigger-nKdmww, .component-cM8DTg.disabled-fZIeN_ .label-rrakc7 {
208
209
  cursor: not-allowed;
209
210
  }
210
211
 
211
- .component-cM8DTg.readOnly-zwkIeK .trigger-nKdmww {
212
+ .component-cM8DTg.readOnly-zwkIeK:not(.disabled-fZIeN_) .trigger-nKdmww {
212
213
  cursor: default;
213
214
  }
214
215
 
@@ -0,0 +1,13 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ export type ContentProps = HTMLAttributes<HTMLDivElement> & {
3
+ /**
4
+ * Базовый идентификатор части -content. По умолчанию наследуется от Sidepanel.
5
+ */
6
+ dataTestId?: string;
7
+ };
8
+ export declare const Content: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
9
+ /**
10
+ * Базовый идентификатор части -content. По умолчанию наследуется от Sidepanel.
11
+ */
12
+ dataTestId?: string;
13
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,21 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { forwardRef, useContext } from "react";
4
+ import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
5
+ import { SidepanelContext } from "../../context.js";
6
+ import index_module from "../../index.module.js";
7
+ import scrollbar_module from "../../scrollbar.module.js";
8
+ import index_module_0 from "./index.module.js";
9
+ const Content = /*#__PURE__*/ forwardRef(({ className, dataTestId, children, ...restProps }, ref)=>{
10
+ const { dataTestId: panelDataTestId } = useContext(SidepanelContext);
11
+ const getDataTestId = useDataTestId(dataTestId ?? panelDataTestId);
12
+ return /*#__PURE__*/ jsx("div", {
13
+ ...restProps,
14
+ ref: ref,
15
+ className: classnames(index_module_0.component, index_module.content, scrollbar_module.scrollbar, className),
16
+ "data-test-id": getDataTestId('content'),
17
+ children: children
18
+ });
19
+ });
20
+ Content.displayName = 'SidepanelContent';
21
+ export { Content };
@@ -0,0 +1,5 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-vYcjBo"
4
+ };
5
+ export default index_module;
@@ -0,0 +1,14 @@
1
+ .component-vYcjBo {
2
+ box-sizing: border-box;
3
+ gap: var(--size-16);
4
+ min-width: 0;
5
+ min-height: 0;
6
+ padding: var(--sidepanel-padding);
7
+ overscroll-behavior: contain;
8
+ overflow-wrap: anywhere;
9
+ flex-direction: column;
10
+ flex: auto;
11
+ display: flex;
12
+ overflow: auto;
13
+ }
14
+
@@ -0,0 +1,66 @@
1
+ import { type ComponentType, type HTMLAttributes } from 'react';
2
+ import type { ButtonProps } from '../../../Button/index.js';
3
+ export type FooterProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML'> & {
4
+ /**
5
+ * Компонент основной кнопки. Обязателен; располагается после secondary (справа в строке).
6
+ * Внешний вид и обработчики задаются внутри переданного компонента.
7
+ */
8
+ primary: ComponentType<ButtonProps>;
9
+ /**
10
+ * Компонент вторичной кнопки. Располагается перед primary (слева в строке).
11
+ * Не выводится, если проп не передан.
12
+ */
13
+ secondary?: ComponentType<ButtonProps>;
14
+ /**
15
+ * Минимальное расстояние между кнопками в пикселях из общей шкалы отступов.
16
+ * @default 12
17
+ */
18
+ gap?: 12 | 16 | 24 | 32;
19
+ /**
20
+ * Оставляет кнопки видимыми при прокрутке. При false Footer прокручивается с Content.
21
+ * @default true
22
+ */
23
+ sticky?: boolean;
24
+ /**
25
+ * Выравнивание кнопок. В space-between кнопки находятся у противоположных краёв,
26
+ * единственная primary — справа. В column secondary находится над primary.
27
+ * @default 'space-between'
28
+ */
29
+ layout?: 'start' | 'end' | 'center' | 'space-between' | 'column';
30
+ /**
31
+ * Базовый идентификатор части -footer. По умолчанию наследуется от Sidepanel.
32
+ */
33
+ dataTestId?: string;
34
+ };
35
+ export declare const Footer: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML"> & {
36
+ /**
37
+ * Компонент основной кнопки. Обязателен; располагается после secondary (справа в строке).
38
+ * Внешний вид и обработчики задаются внутри переданного компонента.
39
+ */
40
+ primary: ComponentType<ButtonProps>;
41
+ /**
42
+ * Компонент вторичной кнопки. Располагается перед primary (слева в строке).
43
+ * Не выводится, если проп не передан.
44
+ */
45
+ secondary?: ComponentType<ButtonProps>;
46
+ /**
47
+ * Минимальное расстояние между кнопками в пикселях из общей шкалы отступов.
48
+ * @default 12
49
+ */
50
+ gap?: 12 | 16 | 24 | 32;
51
+ /**
52
+ * Оставляет кнопки видимыми при прокрутке. При false Footer прокручивается с Content.
53
+ * @default true
54
+ */
55
+ sticky?: boolean;
56
+ /**
57
+ * Выравнивание кнопок. В space-between кнопки находятся у противоположных краёв,
58
+ * единственная primary — справа. В column secondary находится над primary.
59
+ * @default 'space-between'
60
+ */
61
+ layout?: "start" | "end" | "center" | "space-between" | "column";
62
+ /**
63
+ * Базовый идентификатор части -footer. По умолчанию наследуется от Sidepanel.
64
+ */
65
+ dataTestId?: string;
66
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,25 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { forwardRef, useContext } from "react";
4
+ import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
5
+ import { SidepanelContext } from "../../context.js";
6
+ import index_module from "../../index.module.js";
7
+ import index_module_0 from "./index.module.js";
8
+ const Footer = /*#__PURE__*/ forwardRef(({ primary: Primary, secondary: Secondary, gap = 12, sticky = true, layout = 'space-between', className, dataTestId, ...restProps }, ref)=>{
9
+ const { dataTestId: panelDataTestId } = useContext(SidepanelContext);
10
+ const getDataTestId = useDataTestId(dataTestId ?? panelDataTestId);
11
+ return /*#__PURE__*/ jsxs("div", {
12
+ ...restProps,
13
+ ref: ref,
14
+ className: classnames(index_module_0.component, index_module_0[layout], index_module_0[`gap-${gap}`], {
15
+ [index_module.nonStickyFooter]: !sticky
16
+ }, className),
17
+ "data-test-id": getDataTestId('footer'),
18
+ children: [
19
+ Secondary && /*#__PURE__*/ jsx(Secondary, {}),
20
+ /*#__PURE__*/ jsx(Primary, {})
21
+ ]
22
+ });
23
+ });
24
+ Footer.displayName = 'SidepanelFooter';
25
+ export { Footer };
@@ -0,0 +1,19 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-skRGIM",
4
+ column: "column-eFrOSV",
5
+ start: "start-AsGQua",
6
+ end: "end-GUmizA",
7
+ center: "center-ZCtISA",
8
+ "space-between": "space-between-JgT7jT",
9
+ spaceBetween: "space-between-JgT7jT",
10
+ "gap-12": "gap-12-vpXLHP",
11
+ gap12: "gap-12-vpXLHP",
12
+ "gap-16": "gap-16-YnuaQb",
13
+ gap16: "gap-16-YnuaQb",
14
+ "gap-24": "gap-24-Z4lCD9",
15
+ gap24: "gap-24-Z4lCD9",
16
+ "gap-32": "gap-32-dJPD9x",
17
+ gap32: "gap-32-dJPD9x"
18
+ };
19
+ export default index_module;
@@ -0,0 +1,57 @@
1
+ .component-skRGIM {
2
+ box-sizing: border-box;
3
+ padding: var(--sidepanel-padding);
4
+ flex-shrink: 0;
5
+ align-items: center;
6
+ display: flex;
7
+ }
8
+
9
+ .component-skRGIM:not(.column-eFrOSV) > button[data-block="true"] {
10
+ flex: 1 1 0;
11
+ }
12
+
13
+ .start-AsGQua {
14
+ justify-content: flex-start;
15
+ }
16
+
17
+ .end-GUmizA {
18
+ justify-content: flex-end;
19
+ }
20
+
21
+ .center-ZCtISA {
22
+ justify-content: center;
23
+ }
24
+
25
+ .space-between-JgT7jT {
26
+ justify-content: space-between;
27
+ }
28
+
29
+ .space-between-JgT7jT > :only-child {
30
+ margin-left: auto;
31
+ }
32
+
33
+ .column-eFrOSV {
34
+ flex-direction: column;
35
+ align-items: stretch;
36
+ }
37
+
38
+ .column-eFrOSV > button {
39
+ width: 100%;
40
+ }
41
+
42
+ .gap-12-vpXLHP {
43
+ gap: var(--size-12);
44
+ }
45
+
46
+ .gap-16-YnuaQb {
47
+ gap: var(--size-16);
48
+ }
49
+
50
+ .gap-24-Z4lCD9 {
51
+ gap: var(--size-24);
52
+ }
53
+
54
+ .gap-32-dJPD9x {
55
+ gap: var(--size-32);
56
+ }
57
+
@@ -0,0 +1,33 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ export type HeaderProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
3
+ /**
4
+ * Необязательный заголовок панели. Без заголовка и крестика Header не выводится.
5
+ */
6
+ title?: string;
7
+ /**
8
+ * Показывает крестик, который вызывает onClose ближайшего Sidepanel.
9
+ * @default true
10
+ */
11
+ hasCloser?: boolean;
12
+ /**
13
+ * Базовый идентификатор частей -header и -closer.
14
+ * По умолчанию наследуется от Sidepanel.
15
+ */
16
+ dataTestId?: string;
17
+ };
18
+ export declare const Header: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children" | "title"> & {
19
+ /**
20
+ * Необязательный заголовок панели. Без заголовка и крестика Header не выводится.
21
+ */
22
+ title?: string;
23
+ /**
24
+ * Показывает крестик, который вызывает onClose ближайшего Sidepanel.
25
+ * @default true
26
+ */
27
+ hasCloser?: boolean;
28
+ /**
29
+ * Базовый идентификатор частей -header и -closer.
30
+ * По умолчанию наследуется от Sidepanel.
31
+ */
32
+ dataTestId?: string;
33
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,45 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { CrossHeavyMIcon } from "@koslibs/icons/CrossHeavyMIcon";
3
+ import classnames from "classnames";
4
+ import { forwardRef, useContext } from "react";
5
+ import { useDataTestId } from "../../../../hooks/use-data-test-id.js";
6
+ import { Button } from "../../../Button/index.js";
7
+ import { TypographyText } from "../../../Typography/index.js";
8
+ import { SidepanelContext } from "../../context.js";
9
+ import index_module from "../../index.module.js";
10
+ import index_module_0 from "./index.module.js";
11
+ const Header = /*#__PURE__*/ forwardRef(({ title, hasCloser = true, className, dataTestId, ...restProps }, ref)=>{
12
+ const { onClose, dataTestId: panelDataTestId } = useContext(SidepanelContext);
13
+ const getDataTestId = useDataTestId(dataTestId ?? panelDataTestId);
14
+ if (!title?.trim() && !hasCloser) return null;
15
+ return /*#__PURE__*/ jsxs("div", {
16
+ ...restProps,
17
+ ref: ref,
18
+ className: classnames(index_module_0.component, index_module.header, className),
19
+ "data-test-id": getDataTestId('header'),
20
+ children: [
21
+ /*#__PURE__*/ jsx("div", {
22
+ className: index_module_0.title,
23
+ children: title && /*#__PURE__*/ jsx(TypographyText, {
24
+ size: 20,
25
+ weight: "semibold",
26
+ children: title
27
+ })
28
+ }),
29
+ hasCloser && /*#__PURE__*/ jsx(Button, {
30
+ size: 32,
31
+ view: "transparent",
32
+ className: index_module_0.closer,
33
+ labelClassName: index_module_0.closerLabel,
34
+ onClick: onClose,
35
+ dataTestId: getDataTestId('closer'),
36
+ children: /*#__PURE__*/ jsx(CrossHeavyMIcon, {
37
+ width: 24,
38
+ height: 24
39
+ })
40
+ })
41
+ ]
42
+ });
43
+ });
44
+ Header.displayName = 'SidepanelHeader';
45
+ export { Header };
@@ -0,0 +1,8 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-pnHhxm",
4
+ title: "title-nolsR9",
5
+ closer: "closer-PhFI6O",
6
+ closerLabel: "closerLabel-q4m2AK"
7
+ };
8
+ export default index_module;
@@ -0,0 +1,32 @@
1
+ .component-pnHhxm {
2
+ box-sizing: border-box;
3
+ justify-content: space-between;
4
+ align-items: center;
5
+ gap: var(--size-16);
6
+ min-height: var(--size-80);
7
+ padding: var(--sidepanel-padding);
8
+ flex-shrink: 0;
9
+ display: flex;
10
+ }
11
+
12
+ .title-nolsR9 {
13
+ overflow-wrap: anywhere;
14
+ flex: 1;
15
+ min-width: 0;
16
+ }
17
+
18
+ .component-pnHhxm > .closer-PhFI6O {
19
+ width: var(--size-24);
20
+ min-width: var(--size-24);
21
+ height: var(--size-32);
22
+ min-height: var(--size-32);
23
+ padding: var(--size-0);
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .closerLabel-q4m2AK {
28
+ justify-content: center;
29
+ align-items: center;
30
+ display: flex;
31
+ }
32
+
@@ -0,0 +1,4 @@
1
+ import type { SidepanelProps } from '../../types.js';
2
+ export declare const SidepanelSurface: import("react").ForwardRefExoticComponent<Omit<SidepanelProps, "getPortalContainer"> & {
3
+ onExited: VoidFunction;
4
+ } & import("react").RefAttributes<HTMLDialogElement>>;
@@ -0,0 +1,92 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef } from "react";
4
+ import { lockScroll } from "../../../../utils/lock-scroll.js";
5
+ import { SidepanelContext } from "../../context.js";
6
+ import { useDialogTransition } from "../../hooks/use-dialog-transition.js";
7
+ import index_module from "../../index.module.js";
8
+ import scrollbar_module from "../../scrollbar.module.js";
9
+ const SidepanelSurface = /*#__PURE__*/ forwardRef(({ open, onClose, onExited, escapeKeyDown = true, backdropClick = true, lockScroll: shouldLockScroll = true, className, dataTestId, children, onPointerDown, onClick, onTransitionEnd, ...restProps }, ref)=>{
10
+ const dialogRef = useRef(null);
11
+ const backdropPointerDown = useRef(false);
12
+ const mounted = useRef(false);
13
+ useLayoutEffect(()=>{
14
+ mounted.current = true;
15
+ return ()=>{
16
+ mounted.current = false;
17
+ };
18
+ }, []);
19
+ const { active, finishClosing } = useDialogTransition(dialogRef, open, onExited);
20
+ const requestClose = useCallback(()=>{
21
+ if (open) onClose?.();
22
+ }, [
23
+ onClose,
24
+ open
25
+ ]);
26
+ const mergedRef = useCallback((element)=>{
27
+ dialogRef.current = element;
28
+ if ('function' == typeof ref) ref(element);
29
+ else if (ref) ref.current = element;
30
+ }, [
31
+ ref
32
+ ]);
33
+ const context = useMemo(()=>({
34
+ onClose: requestClose,
35
+ dataTestId
36
+ }), [
37
+ dataTestId,
38
+ requestClose
39
+ ]);
40
+ useLayoutEffect(()=>{
41
+ const dialog = dialogRef.current;
42
+ if (active && shouldLockScroll && dialog) return lockScroll(dialog.ownerDocument, true);
43
+ }, [
44
+ active,
45
+ shouldLockScroll
46
+ ]);
47
+ const isBackdrop = (event)=>{
48
+ const bounds = event.currentTarget.getBoundingClientRect();
49
+ return event.target === event.currentTarget && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom);
50
+ };
51
+ const handleCancel = (event)=>{
52
+ event.preventDefault();
53
+ event.stopPropagation();
54
+ if (escapeKeyDown) requestClose();
55
+ };
56
+ const handleClose = (event)=>{
57
+ event.stopPropagation();
58
+ if (mounted.current && !event.currentTarget.open) requestClose();
59
+ };
60
+ const handlePointerDown = (event)=>{
61
+ onPointerDown?.(event);
62
+ backdropPointerDown.current = !event.defaultPrevented && isBackdrop(event);
63
+ };
64
+ const handleClick = (event)=>{
65
+ onClick?.(event);
66
+ const close = backdropPointerDown.current && isBackdrop(event);
67
+ backdropPointerDown.current = false;
68
+ if (backdropClick && close && !event.defaultPrevented) requestClose();
69
+ };
70
+ const handleTransitionEnd = (event)=>{
71
+ onTransitionEnd?.(event);
72
+ if (event.target === event.currentTarget && 'transform' === event.propertyName) finishClosing();
73
+ };
74
+ return /*#__PURE__*/ jsx("dialog", {
75
+ ...restProps,
76
+ ref: mergedRef,
77
+ className: classnames(index_module.component, scrollbar_module.scrollbar, className),
78
+ "data-test-id": dataTestId,
79
+ "data-state": "closed",
80
+ onCancel: handleCancel,
81
+ onClose: handleClose,
82
+ onPointerDown: handlePointerDown,
83
+ onClick: handleClick,
84
+ onTransitionEnd: handleTransitionEnd,
85
+ children: /*#__PURE__*/ jsx(SidepanelContext.Provider, {
86
+ value: context,
87
+ children: children
88
+ })
89
+ });
90
+ });
91
+ SidepanelSurface.displayName = 'SidepanelSurface';
92
+ export { SidepanelSurface };
@@ -0,0 +1,6 @@
1
+ type SidepanelContextValue = {
2
+ onClose?: VoidFunction;
3
+ dataTestId?: string;
4
+ };
5
+ export declare const SidepanelContext: import("react").Context<SidepanelContextValue>;
6
+ export {};
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+ const SidepanelContext = createContext({});
3
+ export { SidepanelContext };
@@ -0,0 +1,5 @@
1
+ import { type RefObject } from 'react';
2
+ export declare function useDialogTransition(dialogRef: RefObject<HTMLDialogElement>, open: boolean, onExited: VoidFunction): {
3
+ active: boolean;
4
+ finishClosing: () => void;
5
+ };
@@ -0,0 +1,83 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
2
+ function milliseconds(value) {
3
+ return (parseFloat(value) || 0) * (value.trim().endsWith('ms') ? 1 : 1000);
4
+ }
5
+ function transitionTime(dialog) {
6
+ const style = dialog.ownerDocument.defaultView?.getComputedStyle(dialog);
7
+ const durations = style?.transitionDuration.split(',').map(milliseconds) ?? [
8
+ 0
9
+ ];
10
+ const delays = style?.transitionDelay.split(',').map(milliseconds) ?? [
11
+ 0
12
+ ];
13
+ return Math.max(0, ...durations.map((duration, index)=>duration + delays[index % delays.length]));
14
+ }
15
+ function useDialogTransition(dialogRef, open, onExited) {
16
+ const [active, setActive] = useState(open);
17
+ const latest = useRef({
18
+ open,
19
+ onExited
20
+ });
21
+ useLayoutEffect(()=>{
22
+ latest.current = {
23
+ open,
24
+ onExited
25
+ };
26
+ }, [
27
+ open,
28
+ onExited
29
+ ]);
30
+ const finishClosing = useCallback(()=>{
31
+ const dialog = dialogRef.current;
32
+ if (!dialog || latest.current.open) return;
33
+ if (dialog.open) dialog.close();
34
+ setActive(false);
35
+ latest.current.onExited();
36
+ }, [
37
+ dialogRef
38
+ ]);
39
+ useLayoutEffect(()=>{
40
+ const dialog = dialogRef.current;
41
+ const view = dialog?.ownerDocument.defaultView;
42
+ if (!dialog || !view) return;
43
+ let frame;
44
+ let timeout;
45
+ if (open) {
46
+ if (!dialog.open) {
47
+ dialog.dataset.state = 'closed';
48
+ dialog.showModal();
49
+ dialog.getBoundingClientRect();
50
+ }
51
+ setActive(true);
52
+ frame = view.requestAnimationFrame(()=>{
53
+ dialog.dataset.state = 'open';
54
+ });
55
+ } else if (dialog.open && 'open' === dialog.dataset.state) {
56
+ dialog.dataset.state = 'closed';
57
+ const duration = transitionTime(dialog);
58
+ if (0 === duration) finishClosing();
59
+ else timeout = view.setTimeout(finishClosing, duration + 50);
60
+ } else finishClosing();
61
+ return ()=>{
62
+ if (void 0 !== frame) view.cancelAnimationFrame(frame);
63
+ if (void 0 !== timeout) view.clearTimeout(timeout);
64
+ };
65
+ }, [
66
+ dialogRef,
67
+ finishClosing,
68
+ open
69
+ ]);
70
+ useLayoutEffect(()=>{
71
+ const dialog = dialogRef.current;
72
+ return ()=>{
73
+ if (dialog?.open) dialog.close();
74
+ };
75
+ }, [
76
+ dialogRef
77
+ ]);
78
+ return {
79
+ active,
80
+ finishClosing
81
+ };
82
+ }
83
+ export { useDialogTransition };
@@ -0,0 +1,8 @@
1
+ export { Sidepanel } from './sidepanel.js';
2
+ export type { SidepanelProps } from './types.js';
3
+ export { Header } from './components/header/index.js';
4
+ export type { HeaderProps } from './components/header/index.js';
5
+ export { Content } from './components/content/index.js';
6
+ export type { ContentProps } from './components/content/index.js';
7
+ export { Footer } from './components/footer/index.js';
8
+ export type { FooterProps } from './components/footer/index.js';
@@ -0,0 +1,4 @@
1
+ export { Sidepanel } from "./sidepanel.js";
2
+ export { Header } from "./components/header/index.js";
3
+ export { Content } from "./components/content/index.js";
4
+ export { Footer } from "./components/footer/index.js";
@@ -0,0 +1,8 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-kIdcz9",
4
+ nonStickyFooter: "nonStickyFooter-rXnHkK",
5
+ header: "header-UlW3Pm",
6
+ content: "content-RhY7Xk"
7
+ };
8
+ export default index_module;
@@ -0,0 +1,68 @@
1
+ @import "../../styles/theme.css";
2
+ @import "./vars.css";
3
+
4
+ .component-kIdcz9 {
5
+ box-sizing: border-box;
6
+ width: 500px;
7
+ min-width: 0;
8
+ max-width: 100vw;
9
+ height: 100dvh;
10
+ max-height: none;
11
+ margin: var(--size-0);
12
+ padding: var(--size-0);
13
+ border-radius: var(--border-radius-0);
14
+ background: var(--sidepanel-background-color);
15
+ color: var(--sidepanel-color);
16
+ font-family: var(--font-primary);
17
+ transition: transform var(--sidepanel-animation-duration) ease-in-out;
18
+ border: 0;
19
+ outline: none;
20
+ flex-direction: column;
21
+ position: fixed;
22
+ inset: 0 0 0 auto;
23
+ overflow: hidden;
24
+ transform: translateX(100%);
25
+
26
+ &[open] {
27
+ display: flex;
28
+ }
29
+
30
+ &:has( > .nonStickyFooter-rXnHkK) {
31
+ overscroll-behavior: contain;
32
+ overflow: auto;
33
+
34
+ & > .header-UlW3Pm {
35
+ z-index: 1;
36
+ background: var(--sidepanel-background-color);
37
+ position: sticky;
38
+ top: 0;
39
+ }
40
+
41
+ & > .content-RhY7Xk {
42
+ flex: 1 0 auto;
43
+ overflow: visible;
44
+ }
45
+ }
46
+
47
+ &[data-state="open"] {
48
+ transform: translateX(0);
49
+ }
50
+
51
+ &::backdrop {
52
+ background: var(--sidepanel-backdrop-color);
53
+ backdrop-filter: blur(var(--sidepanel-backdrop-blur));
54
+ opacity: 0;
55
+ transition: opacity var(--sidepanel-animation-duration) ease-in-out;
56
+ }
57
+
58
+ &[data-state="open"]::backdrop {
59
+ opacity: 1;
60
+ }
61
+ }
62
+
63
+ @media (prefers-reduced-motion: reduce) {
64
+ .component-kIdcz9, .component-kIdcz9::backdrop {
65
+ transition-duration: 0s;
66
+ }
67
+ }
68
+
@@ -0,0 +1,5 @@
1
+ import "./scrollbar_module.css";
2
+ const scrollbar_module = {
3
+ scrollbar: "scrollbar-haYNj_"
4
+ };
5
+ export default scrollbar_module;
@@ -0,0 +1,41 @@
1
+ .scrollbar-haYNj_ {
2
+ scrollbar-color: auto;
3
+ scrollbar-width: auto;
4
+
5
+ &::-webkit-scrollbar {
6
+ width: var(--size-6);
7
+ height: var(--size-6);
8
+ background: none;
9
+ }
10
+
11
+ &::-webkit-scrollbar-button {
12
+ width: 0;
13
+ height: 0;
14
+ display: none;
15
+ }
16
+
17
+ &::-webkit-scrollbar-track {
18
+ background: none;
19
+ }
20
+
21
+ &::-webkit-scrollbar-track-piece {
22
+ background: none;
23
+ }
24
+
25
+ &::-webkit-scrollbar-corner {
26
+ background: none;
27
+ }
28
+
29
+ &::-webkit-scrollbar-thumb {
30
+ border-radius: var(--border-radius-pill);
31
+ background: var(--sidepanel-scrollbar-color);
32
+ }
33
+ }
34
+
35
+ @supports not selector(::-webkit-scrollbar) {
36
+ .scrollbar-haYNj_ {
37
+ scrollbar-width: thin;
38
+ scrollbar-color: var(--sidepanel-scrollbar-color) transparent;
39
+ }
40
+ }
41
+
@@ -0,0 +1,9 @@
1
+ export declare const Sidepanel: import("react").ForwardRefExoticComponent<Omit<import("react").DialogHTMLAttributes<HTMLDialogElement>, "open" | "onClose" | "onCancel"> & {
2
+ open: boolean;
3
+ onClose?: VoidFunction;
4
+ escapeKeyDown?: boolean;
5
+ backdropClick?: boolean;
6
+ lockScroll?: boolean;
7
+ getPortalContainer?: import("../Portal/index.js").PortalProps["getPortalContainer"];
8
+ dataTestId?: string;
9
+ } & import("react").RefAttributes<HTMLDialogElement>>;
@@ -0,0 +1,28 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useLayoutEffect, useState } from "react";
3
+ import { Portal } from "../Portal/index.js";
4
+ import { SidepanelSurface } from "./components/sidepanel-surface/index.js";
5
+ const Sidepanel = /*#__PURE__*/ forwardRef(({ open, getPortalContainer, ...restProps }, ref)=>{
6
+ const [mounted, setMounted] = useState(open);
7
+ useLayoutEffect(()=>{
8
+ if (open) setMounted(true);
9
+ }, [
10
+ open
11
+ ]);
12
+ const handleExited = useCallback(()=>{
13
+ if (!open) setMounted(false);
14
+ }, [
15
+ open
16
+ ]);
17
+ return mounted ? /*#__PURE__*/ jsx(Portal, {
18
+ getPortalContainer: getPortalContainer,
19
+ children: /*#__PURE__*/ jsx(SidepanelSurface, {
20
+ ...restProps,
21
+ ref: ref,
22
+ open: open,
23
+ onExited: handleExited
24
+ })
25
+ }) : null;
26
+ });
27
+ Sidepanel.displayName = 'Sidepanel';
28
+ export { Sidepanel };
@@ -0,0 +1,36 @@
1
+ import type { DialogHTMLAttributes } from 'react';
2
+ import type { PortalProps } from '../Portal/index.js';
3
+ export type SidepanelProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose' | 'onCancel'> & {
4
+ /**
5
+ * Видимость панели. Для закрытия обновите open в обработчике onClose.
6
+ * Панель остаётся открытой до завершения анимации ухода вправо.
7
+ */
8
+ open: boolean;
9
+ /**
10
+ * Запрос закрытия по крестику, Escape или бекдропу.
11
+ */
12
+ onClose?: VoidFunction;
13
+ /**
14
+ * Разрешает запрос закрытия по Escape.
15
+ * @default true
16
+ */
17
+ escapeKeyDown?: boolean;
18
+ /**
19
+ * Разрешает запрос закрытия по бекдропу.
20
+ * @default true
21
+ */
22
+ backdropClick?: boolean;
23
+ /**
24
+ * Блокирует прокрутку страницы до завершения закрытия.
25
+ * @default true
26
+ */
27
+ lockScroll?: boolean;
28
+ /**
29
+ * Контейнер портала. По умолчанию используется body текущего документа.
30
+ */
31
+ getPortalContainer?: PortalProps['getPortalContainer'];
32
+ /**
33
+ * Идентификатор панели. Части получают суффиксы -header, -closer, -content и -footer.
34
+ */
35
+ dataTestId?: string;
36
+ };
File without changes
@@ -0,0 +1,12 @@
1
+ @import "../Modal/vars.css";
2
+
3
+ :root {
4
+ --sidepanel-background-color: var(--modal-background-color);
5
+ --sidepanel-color: var(--modal-color);
6
+ --sidepanel-backdrop-color: var(--modal-backdrop-color);
7
+ --sidepanel-backdrop-blur: var(--modal-backdrop-blur);
8
+ --sidepanel-padding: var(--size-24);
9
+ --sidepanel-scrollbar-color: #a49b89;
10
+ --sidepanel-animation-duration: .2s;
11
+ }
12
+
@@ -83,7 +83,7 @@
83
83
 
84
84
  .tab-yKhKqB:disabled, .tab-yKhKqB:disabled .counter-Tokn9i {
85
85
  color: var(--tabs-disabled-color);
86
- cursor: default;
86
+ cursor: not-allowed;
87
87
  }
88
88
 
89
89
  .tab-yKhKqB:disabled .counter-Tokn9i {
@@ -7,7 +7,6 @@ export declare const Tabs: import("react").ForwardRefExoticComponent<Omit<import
7
7
  selectedId: string;
8
8
  }) => void;
9
9
  size?: 40 | 48;
10
- emptyState?: ReactNode;
11
10
  listClassName?: string;
12
11
  dataTestId?: string;
13
12
  } & import("react").RefAttributes<HTMLDivElement>>;
@@ -40,7 +40,7 @@ function resolveValue(items, value, previous) {
40
40
  const start = Math.max(0, index);
41
41
  return items.slice(start).find((item)=>!item.disabled)?.id ?? items.slice(0, start).reverse().find((item)=>!item.disabled)?.id;
42
42
  }
43
- const Tabs = /*#__PURE__*/ forwardRef(({ children, selectedId, defaultSelectedId, onChange, size = 40, emptyState, listClassName, dataTestId, className, ...restProps }, ref)=>{
43
+ const Tabs = /*#__PURE__*/ forwardRef(({ children, selectedId, defaultSelectedId, onChange, size = 40, listClassName, dataTestId, className, ...restProps }, ref)=>{
44
44
  const getDataTestId = useDataTestId(dataTestId);
45
45
  const items = useMemo(()=>collectTabs(children), [
46
46
  children
@@ -78,12 +78,13 @@ const Tabs = /*#__PURE__*/ forwardRef(({ children, selectedId, defaultSelectedId
78
78
  selectedId: id
79
79
  });
80
80
  };
81
+ if (0 === items.length) return null;
81
82
  return /*#__PURE__*/ jsx("div", {
82
83
  ...restProps,
83
84
  ref: ref,
84
85
  className: classnames(index_module.component, className),
85
86
  "data-test-id": dataTestId,
86
- children: void 0 === selectedValue ? emptyState : /*#__PURE__*/ jsx(TabsContext.Provider, {
87
+ children: /*#__PURE__*/ jsx(TabsContext.Provider, {
87
88
  value: {
88
89
  selectedId: selectedValue,
89
90
  size,
@@ -102,7 +103,7 @@ const Tabs = /*#__PURE__*/ forwardRef(({ children, selectedId, defaultSelectedId
102
103
  items.map((item)=>/*#__PURE__*/ jsx(Tab, {
103
104
  ...item
104
105
  }, item.id)),
105
- /*#__PURE__*/ jsx("div", {
106
+ void 0 !== selectedValue && /*#__PURE__*/ jsx("div", {
106
107
  ref: indicatorRef,
107
108
  className: index_module.indicator,
108
109
  "data-test-id": getDataTestId('indicator')
@@ -45,10 +45,6 @@ export type TabsProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onCha
45
45
  * @default 40
46
46
  */
47
47
  size?: 40 | 48;
48
- /**
49
- * Общее пустое состояние, когда нет доступных вкладок.
50
- */
51
- emptyState?: ReactNode;
52
48
  /**
53
49
  * Класс ряда заголовков.
54
50
  */
@@ -42,6 +42,7 @@
42
42
  --field-label-color: var(--input-disabled-color);
43
43
  --field-placeholder-color: var(--input-disabled-color);
44
44
  --field-icon-color: var(--input-disabled-color);
45
+ cursor: not-allowed;
45
46
  }
46
47
  }
47
48
 
package/dist/index.d.ts CHANGED
@@ -25,4 +25,6 @@ export type { CollapseProps } from './components/Collapse/index.js';
25
25
  export { Modal, ModalComponent, ModalHeader, ModalContent, ModalFooter, ModalControls, } from './components/Modal/index.js';
26
26
  export { Portal } from './components/Portal/index.js';
27
27
  export type { PortalProps } from './components/Portal/index.js';
28
+ export { Sidepanel, Header as SidepanelHeader, Content as SidepanelContent, Footer as SidepanelFooter, } from './components/Sidepanel/index.js';
29
+ export type { SidepanelProps, HeaderProps as SidepanelHeaderProps, ContentProps as SidepanelContentProps, FooterProps as SidepanelFooterProps, } from './components/Sidepanel/index.js';
28
30
  export type { ModalProps, HeaderProps as ModalHeaderProps, ContentProps as ModalContentProps, FooterProps as ModalFooterProps, ControlsProps as ModalControlsProps, } from './components/Modal/index.js';
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export { Popover } from "./components/Popover/index.js";
12
12
  export { Collapse } from "./components/Collapse/index.js";
13
13
  export { Modal, ModalComponent, ModalContent, ModalControls, ModalFooter, ModalHeader } from "./components/Modal/index.js";
14
14
  export { Portal } from "./components/Portal/index.js";
15
+ export { Content as SidepanelContent, Footer as SidepanelFooter, Header as SidepanelHeader, Sidepanel } from "./components/Sidepanel/index.js";
@@ -0,0 +1 @@
1
+ export declare function lockScroll(document: Document, removeScrollbarGutter?: boolean): () => void;
@@ -0,0 +1,49 @@
1
+ const locks = new WeakMap();
2
+ function lockScroll(document, removeScrollbarGutter = false) {
3
+ const { body, documentElement, defaultView } = document;
4
+ const existing = locks.get(document);
5
+ if (existing) existing.count += 1;
6
+ else {
7
+ locks.set(document, {
8
+ count: 1,
9
+ overflow: body.style.overflow,
10
+ paddingRight: body.style.paddingRight,
11
+ paddingLeft: body.style.paddingLeft
12
+ });
13
+ const scrollbarWidth = (defaultView?.innerWidth ?? 0) - documentElement.clientWidth;
14
+ const hasStableGutter = defaultView?.getComputedStyle(documentElement).scrollbarGutter?.split(/\s+/).includes('stable');
15
+ if (scrollbarWidth > 0 && defaultView && !hasStableGutter) {
16
+ const paddingRight = parseFloat(defaultView.getComputedStyle(body).paddingRight) || 0;
17
+ body.style.paddingRight = `${paddingRight + scrollbarWidth}px`;
18
+ }
19
+ body.style.overflow = 'hidden';
20
+ }
21
+ const gutterLock = locks.get(document);
22
+ if (removeScrollbarGutter && !gutterLock.gutter && defaultView?.getComputedStyle(documentElement).scrollbarGutter?.includes('stable')) {
23
+ const before = documentElement.getBoundingClientRect();
24
+ const paddingLeft = parseFloat(defaultView.getComputedStyle(body).paddingLeft) || 0;
25
+ const paddingRight = parseFloat(defaultView.getComputedStyle(body).paddingRight) || 0;
26
+ gutterLock.gutter = {
27
+ value: documentElement.style.getPropertyValue('scrollbar-gutter'),
28
+ priority: documentElement.style.getPropertyPriority('scrollbar-gutter')
29
+ };
30
+ documentElement.style.setProperty('scrollbar-gutter', 'auto', 'important');
31
+ const after = documentElement.getBoundingClientRect();
32
+ body.style.paddingLeft = `${paddingLeft + Math.max(0, before.left - after.left)}px`;
33
+ body.style.paddingRight = `${paddingRight + Math.max(0, after.right - before.right)}px`;
34
+ }
35
+ return ()=>{
36
+ const lock = locks.get(document);
37
+ if (!lock) return;
38
+ lock.count -= 1;
39
+ if (0 === lock.count) {
40
+ body.style.overflow = lock.overflow;
41
+ body.style.paddingRight = lock.paddingRight;
42
+ body.style.paddingLeft = lock.paddingLeft;
43
+ if (lock.gutter) if (lock.gutter.value) documentElement.style.setProperty('scrollbar-gutter', lock.gutter.value, lock.gutter.priority);
44
+ else documentElement.style.removeProperty('scrollbar-gutter');
45
+ locks.delete(document);
46
+ }
47
+ };
48
+ }
49
+ export { lockScroll };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koslibs/components",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Shared React components for koslibs projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- export declare function lockScroll(document: Document): () => void;
@@ -1,31 +0,0 @@
1
- const locks = new WeakMap();
2
- function lockScroll(document) {
3
- const { body, documentElement, defaultView } = document;
4
- const existing = locks.get(document);
5
- if (existing) existing.count += 1;
6
- else {
7
- locks.set(document, {
8
- count: 1,
9
- overflow: body.style.overflow,
10
- paddingRight: body.style.paddingRight
11
- });
12
- const scrollbarWidth = (defaultView?.innerWidth ?? 0) - documentElement.clientWidth;
13
- const hasStableGutter = defaultView?.getComputedStyle(documentElement).scrollbarGutter?.split(/\s+/).includes('stable');
14
- if (scrollbarWidth > 0 && defaultView && !hasStableGutter) {
15
- const paddingRight = parseFloat(defaultView.getComputedStyle(body).paddingRight) || 0;
16
- body.style.paddingRight = `${paddingRight + scrollbarWidth}px`;
17
- }
18
- body.style.overflow = 'hidden';
19
- }
20
- return ()=>{
21
- const lock = locks.get(document);
22
- if (!lock) return;
23
- lock.count -= 1;
24
- if (0 === lock.count) {
25
- body.style.overflow = lock.overflow;
26
- body.style.paddingRight = lock.paddingRight;
27
- locks.delete(document);
28
- }
29
- };
30
- }
31
- export { lockScroll };