@koslibs/components 1.1.0 → 1.3.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,43 @@
1
1
  # @koslibs/components
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#4](https://github.com/koslibs/components/pull/4) [`b37f3b0`](https://github.com/koslibs/components/commit/b37f3b008be8ffea80908557c13ea77d6d55c792) Thanks [@holypower777](https://github.com/holypower777)! - Перенесён Collapse из core-components GullEye с отдельным импортом
8
+ `@koslibs/components/Collapse`, внутренним и внешним управлением, сохранением
9
+ содержимого, анимацией и поддержкой reduced motion. Проп `colors` убран:
10
+ оформление кнопки настраивается через CSS-переменные. Добавлены документация
11
+ с тремя вкладками, Playground и история компонента. Стрелка подключается
12
+ отдельным импортом из `@koslibs/icons`.
13
+
14
+ Подписи `collapsedLabel` и `expandedLabel` не имеют дефолтных значений.
15
+ Добавлены `showRightChevron` (по умолчанию `true`) и `showLeftChevron`
16
+ (по умолчанию `false`): можно независимо показывать шевроны или скрыть оба.
17
+ Левый шеврон смотрит вправо в закрытом состоянии и вниз в раскрытом,
18
+ правый сохраняет поворот вниз → вверх.
19
+
20
+ Комментарии публичных пропсов приведены к единому многострочному формату JSDoc
21
+ с отдельной строкой для `@default`.
22
+
23
+ ## 1.2.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [#3](https://github.com/koslibs/components/pull/3) [`a618f52`](https://github.com/koslibs/components/commit/a618f526010adc6203a708e73fac3906b555ac78) Thanks [@holypower777](https://github.com/holypower777)! - Перенесён Popover из GullEye с отдельным импортом `@koslibs/components/Popover`,
28
+ стилями, Docs и Playground. Сохранены управляемая видимость, позиционирование,
29
+ ограничение размеров и способы закрытия. Портал и позиционирование адаптированы
30
+ для самостоятельного использования в React 18 и 19.
31
+
32
+ Popover управляет позицией и размерами и рендерит children напрямую. Оформление
33
+ и прокрутка содержимого задаются вызывающим компонентом; colors и contentClassName
34
+ не входят в API. Прежнее оформление Storybook-примеров сохранено в их содержимом.
35
+ Тень задаёт сам Popover через CSS-переменную --popover-shadow.
36
+ Offset по умолчанию равен `[0, 0]`, без смещения и зазора между якорем и содержимым.
37
+
38
+ Документация каждого компонента собрана в его папке docs: stories, страницы Docs
39
+ и стили примеров. Папки документации исключены из JS-сборки и деклараций npm-пакета.
40
+
3
41
  ## 1.1.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -39,9 +39,11 @@ src/components/Button/
39
39
  index.ts
40
40
  button.tsx
41
41
  button.module.css
42
- button.stories.tsx
43
- button.docs.tsx
44
42
  CHANGELOG.md
43
+ docs/
44
+ button.stories.tsx
45
+ button.docs.tsx
46
+ examples.module.css
45
47
  ```
46
48
 
47
49
  1. Перенесите компонент в `src/components/<Name>`; имя папки — PascalCase, например `Button`.
@@ -59,7 +61,9 @@ src/components/Button/
59
61
  }
60
62
  ```
61
63
 
62
- 4. Добавьте Docs, Playground и `CHANGELOG.md` по [примеру документации](https://github.com/koslibs/components/blob/main/docs/component-changelogs.md).
64
+ 4. Добавьте Docs и Playground в папку компонента `docs`, а `CHANGELOG.md` оставьте
65
+ рядом с реализацией, по [примеру документации](https://github.com/koslibs/components/blob/main/docs/component-changelogs.md).
66
+ Папки `src/**/docs` исключены из npm-сборки. Общие блоки Storybook находятся в `src/docs`.
63
67
  5. Добавьте тесты поведения. `npm run test:unit` запускает unit-тесты через
64
68
  `koslibs-builder lib:test`; `npm test` также проверяет отдельные импорты.
65
69
  6. Выполните сборку и проверьте реальные пути JS, деклараций и CSS в `dist`.
@@ -0,0 +1,91 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export type CollapseProps = Omit<HTMLAttributes<HTMLDivElement>, 'onTransitionEnd'> & {
3
+ /**
4
+ * Состояние controlled-компонента.
5
+ */
6
+ expanded?: boolean;
7
+ /**
8
+ * Начальное состояние uncontrolled-компонента.
9
+ * @default false
10
+ */
11
+ defaultExpanded?: boolean;
12
+ /**
13
+ * Подпись кнопки, когда содержимое скрыто.
14
+ */
15
+ collapsedLabel?: ReactNode;
16
+ /**
17
+ * Подпись кнопки, когда содержимое раскрыто.
18
+ */
19
+ expandedLabel?: ReactNode;
20
+ /**
21
+ * Показывать шеврон справа от подписи: вниз при закрытии, вверх при раскрытии.
22
+ * @default true
23
+ */
24
+ showRightChevron?: boolean;
25
+ /**
26
+ * Показывать шеврон слева от подписи: вправо при закрытии, вниз при раскрытии.
27
+ * @default false
28
+ */
29
+ showLeftChevron?: boolean;
30
+ /**
31
+ * Запрос изменения состояния. В controlled-режиме обновите expanded.
32
+ */
33
+ onExpandedChange?: (expanded: boolean) => void;
34
+ /**
35
+ * Завершение раскрытия/сворачивания; без анимации вызывается сразу.
36
+ */
37
+ onTransitionEnd?: (expanded: boolean) => void;
38
+ /**
39
+ * Дополнительный класс внутреннего контейнера содержимого.
40
+ */
41
+ expandedContentClassName?: string;
42
+ /**
43
+ * Идентификатор корневого элемента для автоматизированных тестов.
44
+ */
45
+ dataTestId?: string;
46
+ };
47
+ export declare const Collapse: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "onTransitionEnd"> & {
48
+ /**
49
+ * Состояние controlled-компонента.
50
+ */
51
+ expanded?: boolean;
52
+ /**
53
+ * Начальное состояние uncontrolled-компонента.
54
+ * @default false
55
+ */
56
+ defaultExpanded?: boolean;
57
+ /**
58
+ * Подпись кнопки, когда содержимое скрыто.
59
+ */
60
+ collapsedLabel?: ReactNode;
61
+ /**
62
+ * Подпись кнопки, когда содержимое раскрыто.
63
+ */
64
+ expandedLabel?: ReactNode;
65
+ /**
66
+ * Показывать шеврон справа от подписи: вниз при закрытии, вверх при раскрытии.
67
+ * @default true
68
+ */
69
+ showRightChevron?: boolean;
70
+ /**
71
+ * Показывать шеврон слева от подписи: вправо при закрытии, вниз при раскрытии.
72
+ * @default false
73
+ */
74
+ showLeftChevron?: boolean;
75
+ /**
76
+ * Запрос изменения состояния. В controlled-режиме обновите expanded.
77
+ */
78
+ onExpandedChange?: (expanded: boolean) => void;
79
+ /**
80
+ * Завершение раскрытия/сворачивания; без анимации вызывается сразу.
81
+ */
82
+ onTransitionEnd?: (expanded: boolean) => void;
83
+ /**
84
+ * Дополнительный класс внутреннего контейнера содержимого.
85
+ */
86
+ expandedContentClassName?: string;
87
+ /**
88
+ * Идентификатор корневого элемента для автоматизированных тестов.
89
+ */
90
+ dataTestId?: string;
91
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,103 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { ChevronDownIcon } from "@koslibs/icons/ChevronDownIcon";
3
+ import classnames from "classnames";
4
+ import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
5
+ import index_module from "./index.module.js";
6
+ const Collapse = /*#__PURE__*/ forwardRef(({ expanded, defaultExpanded = false, collapsedLabel, expandedLabel, showRightChevron = true, showLeftChevron = false, onExpandedChange, onTransitionEnd, expandedContentClassName, dataTestId, className, children, ...restProps }, ref)=>{
7
+ const [expandedState, setExpandedState] = useState(defaultExpanded);
8
+ const isExpanded = expanded ?? expandedState;
9
+ const contentId = `collapse-${useId()}`;
10
+ const contentRef = useRef(null);
11
+ const innerRef = useRef(null);
12
+ const toggleRef = useRef(null);
13
+ const previousExpanded = useRef(isExpanded);
14
+ const pendingTransition = useRef(false);
15
+ const transitionCallback = useRef(onTransitionEnd);
16
+ const label = isExpanded ? expandedLabel || collapsedLabel : collapsedLabel || expandedLabel;
17
+ useLayoutEffect(()=>{
18
+ transitionCallback.current = onTransitionEnd;
19
+ }, [
20
+ onTransitionEnd
21
+ ]);
22
+ useLayoutEffect(()=>{
23
+ const content = contentRef.current;
24
+ if (!content) return;
25
+ if (!isExpanded && content.contains(content.ownerDocument.activeElement)) toggleRef.current?.focus();
26
+ content.inert = !isExpanded;
27
+ if (previousExpanded.current === isExpanded) return;
28
+ previousExpanded.current = isExpanded;
29
+ pendingTransition.current = true;
30
+ const transitionDuration = content.ownerDocument.defaultView?.getComputedStyle(content).transitionDuration;
31
+ if (innerRef.current?.scrollHeight === 0 || 0 === parseFloat(transitionDuration ?? '0')) {
32
+ pendingTransition.current = false;
33
+ transitionCallback.current?.(isExpanded);
34
+ }
35
+ }, [
36
+ isExpanded
37
+ ]);
38
+ const handleToggle = ()=>{
39
+ const nextExpanded = !isExpanded;
40
+ if (void 0 === expanded) setExpandedState(nextExpanded);
41
+ onExpandedChange?.(nextExpanded);
42
+ };
43
+ const handleTransitionEnd = (event)=>{
44
+ if (event.target === event.currentTarget && 'grid-template-rows' === event.propertyName && pendingTransition.current) {
45
+ pendingTransition.current = false;
46
+ transitionCallback.current?.(isExpanded);
47
+ }
48
+ };
49
+ return /*#__PURE__*/ jsxs("div", {
50
+ ...restProps,
51
+ ref: ref,
52
+ className: classnames(index_module.component, className),
53
+ "data-test-id": dataTestId,
54
+ children: [
55
+ label && /*#__PURE__*/ jsxs("button", {
56
+ ref: toggleRef,
57
+ type: "button",
58
+ className: index_module.toggle,
59
+ "aria-expanded": isExpanded,
60
+ "aria-controls": contentId,
61
+ onClick: handleToggle,
62
+ "data-test-id": dataTestId ? `${dataTestId}-toggle` : void 0,
63
+ children: [
64
+ showLeftChevron && /*#__PURE__*/ jsx(ChevronDownIcon, {
65
+ className: classnames(index_module.icon, index_module.leftIcon, {
66
+ [index_module.rotated]: isExpanded
67
+ }),
68
+ "aria-hidden": "true"
69
+ }),
70
+ /*#__PURE__*/ jsx("span", {
71
+ children: label
72
+ }),
73
+ showRightChevron && /*#__PURE__*/ jsx(ChevronDownIcon, {
74
+ className: classnames(index_module.icon, {
75
+ [index_module.rotated]: isExpanded
76
+ }),
77
+ "aria-hidden": "true"
78
+ })
79
+ ]
80
+ }),
81
+ /*#__PURE__*/ jsx("div", {
82
+ ref: contentRef,
83
+ id: contentId,
84
+ className: classnames(index_module.content, {
85
+ [index_module.expanded]: isExpanded
86
+ }),
87
+ "aria-hidden": !isExpanded,
88
+ "data-test-id": dataTestId ? `${dataTestId}-content` : void 0,
89
+ onTransitionEnd: handleTransitionEnd,
90
+ children: /*#__PURE__*/ jsx("div", {
91
+ className: index_module.clip,
92
+ children: /*#__PURE__*/ jsx("div", {
93
+ ref: innerRef,
94
+ className: expandedContentClassName,
95
+ children: children
96
+ })
97
+ })
98
+ })
99
+ ]
100
+ });
101
+ });
102
+ Collapse.displayName = 'Collapse';
103
+ export { Collapse };
@@ -0,0 +1,2 @@
1
+ export { Collapse } from './collapse.js';
2
+ export type { CollapseProps } from './collapse.js';
@@ -0,0 +1 @@
1
+ export { Collapse } from "./collapse.js";
@@ -0,0 +1,12 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-tPiInR",
4
+ content: "content-D8E_z9",
5
+ expanded: "expanded-rqGMOG",
6
+ clip: "clip-D3ydtQ",
7
+ toggle: "toggle-YA8uJB",
8
+ icon: "icon-wFeaQN",
9
+ rotated: "rotated-sRw5l2",
10
+ leftIcon: "leftIcon-PGZrVM"
11
+ };
12
+ export default index_module;
@@ -0,0 +1,82 @@
1
+ @import "../../styles/theme.css";
2
+ @import "./vars.css";
3
+
4
+ .component-tPiInR {
5
+ min-width: 0;
6
+ font-family: var(--font-primary);
7
+ }
8
+
9
+ .content-D8E_z9 {
10
+ visibility: hidden;
11
+ grid-template-rows: 0fr;
12
+ transition: grid-template-rows .2s, visibility 0s linear .2s;
13
+ display: grid;
14
+ }
15
+
16
+ .expanded-rqGMOG {
17
+ visibility: visible;
18
+ grid-template-rows: 1fr;
19
+ transition-delay: 0s;
20
+ }
21
+
22
+ .clip-D3ydtQ {
23
+ min-height: 0;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .toggle-YA8uJB {
28
+ align-items: center;
29
+ gap: var(--size-4, 4px);
30
+ max-width: 100%;
31
+ padding: var(--size-8, 8px) 0;
32
+ color: var(--collapse-color);
33
+ font: inherit;
34
+ text-align: left;
35
+ overflow-wrap: anywhere;
36
+ cursor: pointer;
37
+ background: none;
38
+ border: 0;
39
+ margin: 0;
40
+ font-size: 13px;
41
+ line-height: 20px;
42
+ display: inline-flex;
43
+
44
+ &:hover {
45
+ color: var(--collapse-hover-color);
46
+ }
47
+
48
+ &:hover span {
49
+ text-underline-offset: 3px;
50
+ text-decoration: underline;
51
+ }
52
+
53
+ &:focus-visible {
54
+ outline: 2px solid var(--collapse-focus-color);
55
+ outline-offset: 2px;
56
+ border-radius: var(--border-radius-4, 4px);
57
+ }
58
+ }
59
+
60
+ .icon-wFeaQN {
61
+ flex-shrink: 0;
62
+ transition: transform .2s;
63
+ }
64
+
65
+ .rotated-sRw5l2 {
66
+ transform: rotate(180deg);
67
+ }
68
+
69
+ .leftIcon-PGZrVM {
70
+ transform: rotate(-90deg);
71
+
72
+ &.rotated-sRw5l2 {
73
+ transform: rotate(0);
74
+ }
75
+ }
76
+
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .content-D8E_z9, .icon-wFeaQN {
79
+ transition: none;
80
+ }
81
+ }
82
+
@@ -0,0 +1,6 @@
1
+ :root {
2
+ --collapse-color: #1c1a18;
3
+ --collapse-hover-color: #686d63;
4
+ --collapse-focus-color: #7967f2;
5
+ }
6
+
@@ -0,0 +1,4 @@
1
+ import type { PopoverProps } from '../../types.js';
2
+ export declare const PopoverSurface: import("react").ForwardRefExoticComponent<Omit<PopoverProps, "open" | "getPortalContainer" | "anchorElement"> & {
3
+ anchorElement: HTMLElement;
4
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,106 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
4
+ import { createSizeModifier } from "../../create-size-modifier.js";
5
+ import { usePopoverPosition } from "../../use-popover-position.js";
6
+ import index_module from "../../index.module.js";
7
+ const DEFAULT_OFFSET = [
8
+ 0,
9
+ 0
10
+ ];
11
+ const PopoverSurface = /*#__PURE__*/ forwardRef(({ anchorElement, position = 'bottom-start', offset = DEFAULT_OFFSET, useAnchorWidth = false, preventFlip = false, fallbackPlacements, preventOverflow = true, availableHeight = true, viewportPadding = 8, onClose, outsideClick = true, escapeKeyDown = true, className, style, children, dataTestId, ...restProps }, ref)=>{
12
+ const [popperElement, setPopperElement] = useState(null);
13
+ useImperativeHandle(ref, ()=>popperElement, [
14
+ popperElement
15
+ ]);
16
+ const padding = Math.max(0, viewportPadding);
17
+ const modifiers = useMemo(()=>[
18
+ {
19
+ name: 'offset',
20
+ options: {
21
+ offset
22
+ }
23
+ },
24
+ {
25
+ name: 'flip',
26
+ enabled: !preventFlip,
27
+ options: {
28
+ fallbackPlacements,
29
+ boundary: [],
30
+ padding
31
+ }
32
+ },
33
+ {
34
+ name: 'preventOverflow',
35
+ enabled: preventOverflow,
36
+ options: {
37
+ boundary: [],
38
+ padding
39
+ }
40
+ },
41
+ createSizeModifier({
42
+ availableHeight,
43
+ useAnchorWidth,
44
+ viewportPadding: padding
45
+ })
46
+ ], [
47
+ offset,
48
+ preventFlip,
49
+ fallbackPlacements,
50
+ preventOverflow,
51
+ availableHeight,
52
+ useAnchorWidth,
53
+ padding
54
+ ]);
55
+ const { styles: popperStyles, attributes, update } = usePopoverPosition(anchorElement, popperElement, position, modifiers);
56
+ useEffect(()=>{
57
+ if (update) update();
58
+ }, [
59
+ children,
60
+ style,
61
+ update
62
+ ]);
63
+ useEffect(()=>{
64
+ if (!onClose) return;
65
+ const ownerDocument = anchorElement.ownerDocument;
66
+ const handlePointerDown = (event)=>{
67
+ if (event.defaultPrevented) return;
68
+ const path = event.composedPath();
69
+ if (path.includes(anchorElement) || popperElement && path.includes(popperElement)) return;
70
+ onClose();
71
+ };
72
+ const handleKeyDown = (event)=>{
73
+ if ('Escape' !== event.key || event.defaultPrevented) return;
74
+ event.preventDefault();
75
+ event.stopPropagation();
76
+ onClose();
77
+ };
78
+ if (outsideClick) ownerDocument.addEventListener('pointerdown', handlePointerDown);
79
+ if (escapeKeyDown) ownerDocument.addEventListener('keydown', handleKeyDown);
80
+ return ()=>{
81
+ ownerDocument.removeEventListener('pointerdown', handlePointerDown);
82
+ ownerDocument.removeEventListener('keydown', handleKeyDown);
83
+ };
84
+ }, [
85
+ anchorElement,
86
+ popperElement,
87
+ onClose,
88
+ outsideClick,
89
+ escapeKeyDown
90
+ ]);
91
+ return /*#__PURE__*/ jsx("div", {
92
+ ...restProps,
93
+ ...attributes,
94
+ ref: setPopperElement,
95
+ className: classnames(index_module.component, className),
96
+ style: {
97
+ ...style,
98
+ ...popperStyles,
99
+ visibility: popperStyles?.transform ? style?.visibility : 'hidden'
100
+ },
101
+ "data-test-id": dataTestId,
102
+ children: children
103
+ });
104
+ });
105
+ PopoverSurface.displayName = 'PopoverSurface';
106
+ export { PopoverSurface };
@@ -0,0 +1,8 @@
1
+ import { type Modifier } from '@popperjs/core';
2
+ type Options = {
3
+ availableHeight: boolean;
4
+ useAnchorWidth: boolean;
5
+ viewportPadding: number;
6
+ };
7
+ export declare const createSizeModifier: (options: Options) => Modifier<"availableSize", Options>;
8
+ export {};
@@ -0,0 +1,26 @@
1
+ import { detectOverflow } from "@popperjs/core";
2
+ const createSizeModifier = (options)=>({
3
+ name: 'availableSize',
4
+ enabled: true,
5
+ phase: 'beforeWrite',
6
+ requires: [
7
+ 'computeStyles'
8
+ ],
9
+ options,
10
+ fn ({ state }) {
11
+ const { availableHeight, useAnchorWidth, viewportPadding } = options;
12
+ const documentElement = state.elements.popper.ownerDocument.documentElement;
13
+ const overflow = detectOverflow(state, {
14
+ boundary: [],
15
+ padding: viewportPadding
16
+ });
17
+ const side = state.placement.split('-')[0];
18
+ const height = 'top' === side || 'bottom' === side ? state.rects.popper.height - overflow[side] : documentElement.clientHeight - 2 * viewportPadding;
19
+ Object.assign(state.styles.popper, {
20
+ '--popover-viewport-padding': `${viewportPadding}px`,
21
+ '--popover-available-height': availableHeight ? `${Math.max(0, height)}px` : 'none'
22
+ });
23
+ if (useAnchorWidth) state.styles.popper.width = `${state.rects.reference.width}px`;
24
+ }
25
+ });
26
+ export { createSizeModifier };
@@ -0,0 +1,2 @@
1
+ export { Popover } from './popover.js';
2
+ export type { PopoverProps, PopoverPosition } from './types.js';
@@ -0,0 +1 @@
1
+ export { Popover } from "./popover.js";
@@ -0,0 +1,5 @@
1
+ import "./index_module.css";
2
+ const index_module = {
3
+ component: "component-wBbNPp"
4
+ };
5
+ export default index_module;
@@ -0,0 +1,15 @@
1
+ @import "./vars.css";
2
+
3
+ .component-wBbNPp {
4
+ box-sizing: border-box;
5
+ z-index: var(--popover-z-index);
6
+ max-width: calc(100vw - var(--popover-viewport-padding, 8px) * 2);
7
+ min-width: 0;
8
+ min-height: 0;
9
+ max-height: var(--popover-available-height, none);
10
+ box-shadow: var(--popover-shadow);
11
+ flex-direction: column;
12
+ display: flex;
13
+ position: fixed;
14
+ }
15
+
@@ -0,0 +1,21 @@
1
+ export type { PopoverProps, PopoverPosition } from './types.js';
2
+ export declare const Popover: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLDivElement>, "className" | "style" | "children"> & {
3
+ children?: import("react").ReactNode;
4
+ className?: string;
5
+ style?: import("react").CSSProperties;
6
+ open: boolean;
7
+ anchorElement: HTMLElement | null;
8
+ position?: import("./types.js").PopoverPosition;
9
+ offset?: [number, number];
10
+ useAnchorWidth?: boolean;
11
+ preventFlip?: boolean;
12
+ fallbackPlacements?: import("./types.js").PopoverPosition[];
13
+ preventOverflow?: boolean;
14
+ availableHeight?: boolean;
15
+ viewportPadding?: number;
16
+ onClose?: VoidFunction;
17
+ outsideClick?: boolean;
18
+ escapeKeyDown?: boolean;
19
+ getPortalContainer?: () => Element | null;
20
+ dataTestId?: string;
21
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,15 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { PopoverSurface } from "./components/popover-surface/index.js";
5
+ const Popover = /*#__PURE__*/ forwardRef(({ open, anchorElement, getPortalContainer, ...restProps }, ref)=>{
6
+ if (!open || !anchorElement) return null;
7
+ const container = getPortalContainer ? getPortalContainer() : anchorElement.ownerDocument.body;
8
+ return container ? /*#__PURE__*/ createPortal(/*#__PURE__*/ jsx(PopoverSurface, {
9
+ ...restProps,
10
+ anchorElement: anchorElement,
11
+ ref: ref
12
+ }), container) : null;
13
+ });
14
+ Popover.displayName = 'Popover';
15
+ export { Popover };
@@ -0,0 +1,88 @@
1
+ import type { Placement } from '@popperjs/core';
2
+ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
3
+ export type PopoverPosition = Exclude<Placement, 'auto' | 'auto-start' | 'auto-end'>;
4
+ export type PopoverProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'className' | 'style'> & {
5
+ /**
6
+ * Содержимое поповера. Оформление и прокрутка задаются вызывающим компонентом.
7
+ */
8
+ children?: ReactNode;
9
+ /**
10
+ * Дополнительный класс внешнего контейнера.
11
+ */
12
+ className?: string;
13
+ /**
14
+ * Стили внешнего контейнера, включая ширину.
15
+ */
16
+ style?: CSSProperties;
17
+ /**
18
+ * Видимость поповера.
19
+ */
20
+ open: boolean;
21
+ /**
22
+ * Элемент, относительно которого открывается поповер.
23
+ */
24
+ anchorElement: HTMLElement | null;
25
+ /**
26
+ * Предпочтительная позиция относительно элемента.
27
+ * @default 'bottom-start'
28
+ */
29
+ position?: PopoverPosition;
30
+ /**
31
+ * Смещение вдоль элемента и расстояние от него, в пикселях.
32
+ * @default [0, 0]
33
+ */
34
+ offset?: [number, number];
35
+ /**
36
+ * Использовать ширину элемента, относительно которого открыт поповер.
37
+ * @default false
38
+ */
39
+ useAnchorWidth?: boolean;
40
+ /**
41
+ * Запретить смену стороны при недостатке места.
42
+ * @default false
43
+ */
44
+ preventFlip?: boolean;
45
+ /**
46
+ * Позиции, которые проверяются при недостатке места.
47
+ */
48
+ fallbackPlacements?: PopoverPosition[];
49
+ /**
50
+ * Удерживать поповер в пределах видимой области.
51
+ * @default true
52
+ */
53
+ preventOverflow?: boolean;
54
+ /**
55
+ * Ограничивать высоту доступным местом.
56
+ * Для прокрутки задайте содержимому min-height: 0 и overflow: auto.
57
+ * @default true
58
+ */
59
+ availableHeight?: boolean;
60
+ /**
61
+ * Минимальный отступ от границ видимой области, в пикселях.
62
+ * @default 8
63
+ */
64
+ viewportPadding?: number;
65
+ /**
66
+ * Обработчик запроса закрытия. Обновите open в вызывающем компоненте.
67
+ */
68
+ onClose?: VoidFunction;
69
+ /**
70
+ * Разрешить закрытие по нажатию вне поповера и его элемента.
71
+ * @default true
72
+ */
73
+ outsideClick?: boolean;
74
+ /**
75
+ * Разрешить закрытие по Escape.
76
+ * @default true
77
+ */
78
+ escapeKeyDown?: boolean;
79
+ /**
80
+ * Контейнер портала. По умолчанию — body документа anchorElement.
81
+ * Если функция возвращает null, поповер не рендерится.
82
+ */
83
+ getPortalContainer?: () => Element | null;
84
+ /**
85
+ * Идентификатор для автоматизированных тестов.
86
+ */
87
+ dataTestId?: string;
88
+ };
File without changes
@@ -0,0 +1,8 @@
1
+ import { type Modifier } from '@popperjs/core';
2
+ import { type CSSProperties } from 'react';
3
+ import type { PopoverPosition } from './types.js';
4
+ export declare function usePopoverPosition(anchorElement: HTMLElement, popperElement: HTMLDivElement | null, position: PopoverPosition, modifiers: Partial<Modifier<string, object>>[]): {
5
+ update: () => Promise<Partial<import("@popperjs/core").State>> | undefined;
6
+ styles: CSSProperties;
7
+ attributes: Record<string, string | boolean>;
8
+ };
@@ -0,0 +1,78 @@
1
+ import { createPopper } from "@popperjs/core";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ const initialState = {
4
+ styles: {
5
+ position: 'fixed',
6
+ left: 0,
7
+ top: 0,
8
+ visibility: 'hidden'
9
+ },
10
+ attributes: {}
11
+ };
12
+ function usePopoverPosition(anchorElement, popperElement, position, modifiers) {
13
+ const [state, setState] = useState(initialState);
14
+ const instanceRef = useRef(null);
15
+ const updateState = useMemo(()=>({
16
+ name: 'reactStyles',
17
+ enabled: true,
18
+ phase: 'write',
19
+ requires: [
20
+ 'computeStyles'
21
+ ],
22
+ fn: ({ state: popperState })=>{
23
+ setState({
24
+ styles: {
25
+ ...popperState.styles.popper
26
+ },
27
+ attributes: {
28
+ ...popperState.attributes.popper
29
+ }
30
+ });
31
+ }
32
+ }), []);
33
+ useEffect(()=>{
34
+ if (!popperElement) return;
35
+ let active = true;
36
+ setState(initialState);
37
+ const instance = createPopper(anchorElement, popperElement, {
38
+ placement: position,
39
+ strategy: 'fixed',
40
+ modifiers: [
41
+ ...modifiers,
42
+ {
43
+ ...updateState,
44
+ fn: (args)=>{
45
+ if (active) updateState.fn(args);
46
+ }
47
+ },
48
+ {
49
+ name: 'applyStyles',
50
+ enabled: false
51
+ }
52
+ ]
53
+ });
54
+ instanceRef.current = instance;
55
+ const ResizeObserverClass = anchorElement.ownerDocument.defaultView?.ResizeObserver;
56
+ const observer = ResizeObserverClass ? new ResizeObserverClass(()=>void instance.update()) : void 0;
57
+ observer?.observe(anchorElement);
58
+ observer?.observe(popperElement);
59
+ return ()=>{
60
+ active = false;
61
+ observer?.disconnect();
62
+ instance.destroy();
63
+ instanceRef.current = null;
64
+ };
65
+ }, [
66
+ anchorElement,
67
+ popperElement,
68
+ position,
69
+ modifiers,
70
+ updateState
71
+ ]);
72
+ const update = useCallback(()=>instanceRef.current?.update(), []);
73
+ return {
74
+ ...state,
75
+ update
76
+ };
77
+ }
78
+ export { usePopoverPosition };
@@ -0,0 +1,5 @@
1
+ :root {
2
+ --popover-z-index: 1100;
3
+ --popover-shadow: 0 8px 24px #00000029;
4
+ }
5
+
@@ -2,11 +2,17 @@ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
2
2
  export type TypographyWeight = 'regular' | 'medium' | 'semibold' | 'bold';
3
3
  export type TypographyColor = 'inherit' | 'primary' | 'secondary' | 'inverse' | 'positive' | 'negative';
4
4
  export type BaseProps = Omit<HTMLAttributes<HTMLElement>, 'color' | 'children' | 'className' | 'style'> & {
5
- /** Содержимое текста или заголовка. */
5
+ /**
6
+ * Содержимое текста или заголовка.
7
+ */
6
8
  children?: ReactNode;
7
- /** Дополнительный CSS-класс корневого элемента. */
9
+ /**
10
+ * Дополнительный CSS-класс корневого элемента.
11
+ */
8
12
  className?: string;
9
- /** Инлайн-стили корневого элемента. */
13
+ /**
14
+ * Инлайн-стили корневого элемента.
15
+ */
10
16
  style?: CSSProperties;
11
17
  /**
12
18
  * Цвет текста. По умолчанию наследуется от родителя.
package/dist/index.d.ts CHANGED
@@ -4,3 +4,7 @@ export { Spinner } from './components/Spinner/index.js';
4
4
  export type { SpinnerProps } from './components/Spinner/index.js';
5
5
  export { Typography, TypographyText, TypographyTitle, Text, Title } from './components/Typography/index.js';
6
6
  export type { TextProps, TitleProps, TypographyColor, TypographyWeight, } from './components/Typography/index.js';
7
+ export { Popover } from './components/Popover/index.js';
8
+ export type { PopoverProps, PopoverPosition } from './components/Popover/index.js';
9
+ export { Collapse } from './components/Collapse/index.js';
10
+ export type { CollapseProps } from './components/Collapse/index.js';
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { Button } from "./components/Button/index.js";
2
2
  export { Spinner } from "./components/Spinner/index.js";
3
3
  export { Text, Title, Typography, TypographyText, TypographyTitle } from "./components/Typography/index.js";
4
+ export { Popover } from "./components/Popover/index.js";
5
+ export { Collapse } from "./components/Collapse/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koslibs/components",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shared React components for koslibs projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -80,6 +80,8 @@
80
80
  "react-dom": "^18.3.1 || ^19.0.0"
81
81
  },
82
82
  "dependencies": {
83
+ "@koslibs/icons": "1.0.1",
84
+ "@popperjs/core": "2.11.8",
83
85
  "classnames": "2.5.1"
84
86
  }
85
87
  }