@cloud-ru/uikit-product-page-layout 0.21.5 → 0.22.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 (78) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/components/DefaultSubHeader/DefaultSubHeader.d.ts +2 -2
  3. package/dist/cjs/components/DefaultSubHeader/DefaultSubHeader.js +2 -2
  4. package/dist/cjs/components/Headline/Headline.d.ts +1 -1
  5. package/dist/cjs/components/Headline/Headline.js +1 -1
  6. package/dist/cjs/components/PageCatalog/PageCatalog.d.ts +1 -1
  7. package/dist/cjs/components/PageCatalog/PageCatalog.js +1 -1
  8. package/dist/cjs/components/PageForm/PageForm.d.ts +1 -1
  9. package/dist/cjs/components/PageForm/PageForm.js +1 -1
  10. package/dist/cjs/components/PageForm/hooks.js +1 -1
  11. package/dist/cjs/components/PageLoading/PageLoading.d.ts +1 -1
  12. package/dist/cjs/components/PageLoading/PageLoading.js +1 -1
  13. package/dist/cjs/components/PageServices/PageServices.d.ts +1 -1
  14. package/dist/cjs/components/PageSidebar/PageSidebar.js +1 -1
  15. package/dist/cjs/components/PageSidebar/helperComponents/SidebarSearch/SidebarSearch.js +1 -1
  16. package/dist/cjs/components/PageSidebar/helperComponents/SidebarSearchToggle/SidebarSearchToggle.js +2 -2
  17. package/dist/cjs/components/PageSidebar/hooks/useItemsCreator.js +2 -2
  18. package/dist/cjs/components/TreeNavigation/TreeNavigation.d.ts +42 -0
  19. package/dist/cjs/components/TreeNavigation/TreeNavigation.js +23 -0
  20. package/dist/cjs/components/TreeNavigation/helper-components/ConditionalPopover/ConditionalPopover.d.ts +10 -0
  21. package/dist/cjs/components/TreeNavigation/helper-components/ConditionalPopover/ConditionalPopover.js +18 -0
  22. package/dist/cjs/components/TreeNavigation/helper-components/ConditionalPopover/styles.module.css +6 -0
  23. package/dist/cjs/components/TreeNavigation/helper-components/Menu/Menu.d.ts +9 -0
  24. package/dist/cjs/components/TreeNavigation/helper-components/Menu/Menu.js +25 -0
  25. package/dist/cjs/components/TreeNavigation/helper-components/Menu/styles.module.css +12 -0
  26. package/dist/cjs/components/TreeNavigation/helper-components/Menu/utils.d.ts +7 -0
  27. package/dist/cjs/components/TreeNavigation/helper-components/Menu/utils.js +20 -0
  28. package/dist/cjs/components/TreeNavigation/helper-components/index.d.ts +2 -0
  29. package/dist/cjs/components/TreeNavigation/helper-components/index.js +7 -0
  30. package/dist/cjs/components/TreeNavigation/index.d.ts +1 -0
  31. package/dist/cjs/components/TreeNavigation/index.js +17 -0
  32. package/dist/cjs/components/TreeNavigation/styles.module.css +82 -0
  33. package/dist/cjs/components/index.d.ts +1 -0
  34. package/dist/cjs/components/index.js +1 -0
  35. package/dist/esm/components/DefaultSubHeader/DefaultSubHeader.d.ts +2 -2
  36. package/dist/esm/components/DefaultSubHeader/DefaultSubHeader.js +2 -2
  37. package/dist/esm/components/Headline/Headline.d.ts +1 -1
  38. package/dist/esm/components/Headline/Headline.js +1 -1
  39. package/dist/esm/components/PageCatalog/PageCatalog.d.ts +1 -1
  40. package/dist/esm/components/PageCatalog/PageCatalog.js +1 -1
  41. package/dist/esm/components/PageForm/PageForm.d.ts +1 -1
  42. package/dist/esm/components/PageForm/PageForm.js +1 -1
  43. package/dist/esm/components/PageForm/hooks.js +1 -1
  44. package/dist/esm/components/PageLoading/PageLoading.d.ts +1 -1
  45. package/dist/esm/components/PageLoading/PageLoading.js +1 -1
  46. package/dist/esm/components/PageServices/PageServices.d.ts +1 -1
  47. package/dist/esm/components/PageSidebar/PageSidebar.js +1 -1
  48. package/dist/esm/components/PageSidebar/helperComponents/SidebarSearch/SidebarSearch.js +1 -1
  49. package/dist/esm/components/PageSidebar/helperComponents/SidebarSearchToggle/SidebarSearchToggle.js +2 -2
  50. package/dist/esm/components/PageSidebar/hooks/useItemsCreator.js +2 -2
  51. package/dist/esm/components/TreeNavigation/TreeNavigation.d.ts +42 -0
  52. package/dist/esm/components/TreeNavigation/TreeNavigation.js +17 -0
  53. package/dist/esm/components/TreeNavigation/helper-components/ConditionalPopover/ConditionalPopover.d.ts +10 -0
  54. package/dist/esm/components/TreeNavigation/helper-components/ConditionalPopover/ConditionalPopover.js +12 -0
  55. package/dist/esm/components/TreeNavigation/helper-components/ConditionalPopover/styles.module.css +6 -0
  56. package/dist/esm/components/TreeNavigation/helper-components/Menu/Menu.d.ts +9 -0
  57. package/dist/esm/components/TreeNavigation/helper-components/Menu/Menu.js +19 -0
  58. package/dist/esm/components/TreeNavigation/helper-components/Menu/styles.module.css +12 -0
  59. package/dist/esm/components/TreeNavigation/helper-components/Menu/utils.d.ts +7 -0
  60. package/dist/esm/components/TreeNavigation/helper-components/Menu/utils.js +16 -0
  61. package/dist/esm/components/TreeNavigation/helper-components/index.d.ts +2 -0
  62. package/dist/esm/components/TreeNavigation/helper-components/index.js +2 -0
  63. package/dist/esm/components/TreeNavigation/index.d.ts +1 -0
  64. package/dist/esm/components/TreeNavigation/index.js +1 -0
  65. package/dist/esm/components/TreeNavigation/styles.module.css +82 -0
  66. package/dist/esm/components/index.d.ts +1 -0
  67. package/dist/esm/components/index.js +1 -0
  68. package/package.json +8 -6
  69. package/src/components/TreeNavigation/TreeNavigation.tsx +128 -0
  70. package/src/components/TreeNavigation/helper-components/ConditionalPopover/ConditionalPopover.tsx +34 -0
  71. package/src/components/TreeNavigation/helper-components/ConditionalPopover/styles.module.scss +7 -0
  72. package/src/components/TreeNavigation/helper-components/Menu/Menu.tsx +50 -0
  73. package/src/components/TreeNavigation/helper-components/Menu/styles.module.scss +14 -0
  74. package/src/components/TreeNavigation/helper-components/Menu/utils.ts +22 -0
  75. package/src/components/TreeNavigation/helper-components/index.ts +2 -0
  76. package/src/components/TreeNavigation/index.ts +1 -0
  77. package/src/components/TreeNavigation/styles.module.scss +86 -0
  78. package/src/components/index.ts +1 -0
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import cn from 'classnames';
14
14
  import { useCallback, useState } from 'react';
15
15
  import { useUncontrolledProp } from 'uncontrollable';
16
- import { VerticalMenuCloseSVG, VerticalMenuOpenSVG } from '@sbercloud/uikit-product-icons';
16
+ import { VerticalMenuCloseSVG, VerticalMenuOpenSVG } from '@cloud-ru/uikit-product-icons';
17
17
  import { ButtonElevated } from '@snack-uikit/button';
18
18
  import { List } from '@snack-uikit/list';
19
19
  import { extractSupportProps } from '@snack-uikit/utils';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
- import { useLocale } from '@sbercloud/uikit-product-locale';
3
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
4
4
  import { Search } from '@snack-uikit/search';
5
5
  import { useSearchContext } from '../../contexts';
6
6
  import styles from './styles.module.css';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { SearchSVG, VerticalMenuRightCloseSVG } from '@sbercloud/uikit-product-icons';
3
- import { useLocale } from '@sbercloud/uikit-product-locale';
2
+ import { SearchSVG, VerticalMenuRightCloseSVG } from '@cloud-ru/uikit-product-icons';
3
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
4
4
  import { ButtonFunction } from '@snack-uikit/button';
5
5
  import { Tooltip } from '@snack-uikit/tooltip';
6
6
  import { useSearchContext } from '../../contexts';
@@ -11,8 +11,8 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { useMemo } from 'react';
14
- import { ChevronLeftSVG } from '@sbercloud/uikit-product-icons';
15
- import { useLocale } from '@sbercloud/uikit-product-locale';
14
+ import { ChevronLeftSVG } from '@cloud-ru/uikit-product-icons';
15
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
16
16
  import { Tooltip } from '@snack-uikit/tooltip';
17
17
  import { SidebarSearchToggle } from '../helperComponents/SidebarSearchToggle';
18
18
  import { SidebarTitle } from '../helperComponents/SidebarTitle';
@@ -0,0 +1,42 @@
1
+ import { ReactNode } from 'react';
2
+ import { StatusProps } from '@snack-uikit/status';
3
+ import { TreeNodeProps } from '@snack-uikit/tree';
4
+ export type TreeNavigationProps = {
5
+ header: {
6
+ /** Текст заголовка */
7
+ title: string;
8
+ /** Иконка */
9
+ icon?: ReactNode;
10
+ /** Текст описания */
11
+ description?: string;
12
+ /** Статус (цвет, иконка и т.п.) – любой тип, который принимает ваш <Status/> */
13
+ status?: StatusProps;
14
+ /** Раздел для действий */
15
+ actions?: ReactNode;
16
+ };
17
+ menu: {
18
+ /** Заголовок меню */
19
+ menuTitle?: string;
20
+ /** Данные для дерева меню */
21
+ items: TreeNodeProps[];
22
+ /** Управляемый режим: если передан, меню открывается как popover. */
23
+ isMenuOpen?: boolean;
24
+ /** Колбэк, вызываемый при попытке изменить состояние меню.
25
+ * В контролируемом режиме обязателен, в неконтролируемом – опционален.
26
+ */
27
+ onMenuToggle?: (open: boolean) => void;
28
+ /** Открывать меню по умолчанию */
29
+ defaultMenuOpened?: boolean;
30
+ /** Позляет отключить кнопку "Свернуть все"*/
31
+ enableShrinkMenuButton?: boolean;
32
+ /** Открывать пункты меню по умолчанию */
33
+ withDefaultOpenedMenuList?: boolean;
34
+ };
35
+ /** Контентная часть страницы */
36
+ content: ReactNode;
37
+ /** Вариант отображения */
38
+ mode: 'popover' | 'aside';
39
+ /** Класс для контейнера контентной части */
40
+ contentClassName?: string;
41
+ };
42
+ export declare function TreeNavigation({ header: { title, icon, description, status, actions }, menu: { menuTitle, items, enableShrinkMenuButton, withDefaultOpenedMenuList, isMenuOpen, defaultMenuOpened, onMenuToggle, }, content, mode, contentClassName, }: TreeNavigationProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import cn from 'classnames';
3
+ import { useMemo } from 'react';
4
+ import { useUncontrolledProp } from 'uncontrollable';
5
+ import { BurgerSVG, CloseSVG } from '@cloud-ru/uikit-product-icons';
6
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
7
+ import { ButtonSimple } from '@snack-uikit/button';
8
+ import { Status } from '@snack-uikit/status';
9
+ import { Typography } from '@snack-uikit/typography';
10
+ import { ConditionalPopover, Menu } from './helper-components';
11
+ import styles from './styles.module.css';
12
+ export function TreeNavigation({ header: { title, icon, description, status, actions }, menu: { menuTitle, items, enableShrinkMenuButton, withDefaultOpenedMenuList, isMenuOpen, defaultMenuOpened, onMenuToggle, }, content, mode, contentClassName, }) {
13
+ const [open, setOpen] = useUncontrolledProp(isMenuOpen, defaultMenuOpened, onMenuToggle);
14
+ const { t } = useLocale('PageLayout');
15
+ const menu = useMemo(() => (_jsx(Menu, { menuItems: items, menuTitle: menuTitle, enableShrinkMenuButton: enableShrinkMenuButton, withDefaultOpenedMenuList: withDefaultOpenedMenuList })), [items, menuTitle, enableShrinkMenuButton, withDefaultOpenedMenuList]);
16
+ return (_jsxs("div", { className: styles.root, children: [_jsxs("div", { className: styles.header, children: [_jsxs("div", { className: styles.titleWrapper, children: [_jsxs("div", { className: styles.titleInner, children: [_jsx(ConditionalPopover, { isOpen: Boolean(open), onOpenChange: setOpen, tip: menu, withPopover: mode === 'popover', children: _jsx("div", { className: styles.innerElement, children: _jsx(ButtonSimple, { size: 'xs', "aria-label": open ? t('TreeNavigation.closeMenu') : t('TreeNavigation.openMenu'), icon: open ? _jsx(CloseSVG, {}) : _jsx(BurgerSVG, {}), onClick: () => setOpen(!open) }) }) }), icon && (_jsx("div", { className: styles.innerElement, children: _jsx("div", { className: styles.icon, children: icon }) })), _jsx(Typography.SansTitleL, { className: styles.title, children: title }), status && (_jsx("div", { className: styles.innerElement, children: _jsx(Status, Object.assign({}, status)) }))] }), description && _jsx(Typography.SansBodyS, { className: styles.description, children: description })] }), actions] }), _jsxs("div", { className: styles.body, children: [mode === 'aside' && open && _jsx("aside", { className: styles.sidebar, children: menu }), _jsx("div", { className: cn(styles.main, contentClassName), children: content })] })] }));
17
+ }
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ type ConditionalPopoverProps = {
3
+ isOpen: boolean;
4
+ onOpenChange: (value: boolean) => void;
5
+ tip: ReactNode;
6
+ withPopover?: boolean;
7
+ children: ReactNode;
8
+ };
9
+ export declare function ConditionalPopover({ tip, withPopover, isOpen, onOpenChange, children }: ConditionalPopoverProps): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
10
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Popover } from '@snack-uikit/popover';
3
+ import styles from './styles.module.css';
4
+ export function ConditionalPopover({ tip, withPopover, isOpen, onOpenChange, children }) {
5
+ if (withPopover) {
6
+ return (_jsx(Popover, { className: styles.popover, open: isOpen, onOpenChange: () => {
7
+ if (!open)
8
+ onOpenChange(false);
9
+ }, tip: tip, trigger: 'click', placement: 'bottom-start', children: children }));
10
+ }
11
+ return children;
12
+ }
@@ -0,0 +1,6 @@
1
+ .popover{
2
+ max-height:80%;
3
+ }
4
+ .popover > div{
5
+ overflow:auto;
6
+ }
@@ -0,0 +1,9 @@
1
+ import { TreeNodeProps } from '@snack-uikit/tree';
2
+ type MenuProps = {
3
+ menuTitle?: string;
4
+ menuItems: TreeNodeProps[];
5
+ enableShrinkMenuButton?: boolean;
6
+ withDefaultOpenedMenuList?: boolean;
7
+ };
8
+ export declare function Menu({ menuTitle, menuItems, enableShrinkMenuButton, withDefaultOpenedMenuList }: MenuProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { HorizontalMenuCloseSVG } from '@cloud-ru/uikit-product-icons';
4
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
5
+ import { ButtonSimple } from '@snack-uikit/button';
6
+ import { Tree } from '@snack-uikit/tree';
7
+ import { Typography } from '@snack-uikit/typography';
8
+ import styles from './styles.module.css';
9
+ import { getExpandedNodes } from './utils';
10
+ export function Menu({ menuTitle, menuItems, enableShrinkMenuButton = true, withDefaultOpenedMenuList }) {
11
+ const { t } = useLocale('PageLayout');
12
+ const allExpandedNodes = useMemo(() => getExpandedNodes(menuItems), [menuItems]);
13
+ const [expandedNodes, setExpandedNodes] = useState(withDefaultOpenedMenuList ? allExpandedNodes : []);
14
+ const isExpanded = expandedNodes.length > 0;
15
+ const handleExpandAll = () => setExpandedNodes(getExpandedNodes(menuItems));
16
+ const handleCollapseAll = () => setExpandedNodes([]);
17
+ const showSubheader = Boolean(menuTitle || enableShrinkMenuButton);
18
+ return (_jsxs("div", { className: styles.sidebar, children: [showSubheader && (_jsxs("div", { className: styles.subheader, children: [_jsx(Typography.SansTitleM, { children: menuTitle }), enableShrinkMenuButton && (_jsx(ButtonSimple, { label: isExpanded ? t('TreeNavigation.collapseAll') : t('TreeNavigation.expandAll'), icon: _jsx(HorizontalMenuCloseSVG, {}), onClick: isExpanded ? handleCollapseAll : handleExpandAll }))] })), _jsx(Tree, { data: menuItems, selectionMode: 'single', expandedNodes: expandedNodes, onExpand: setExpandedNodes })] }));
19
+ }
@@ -0,0 +1,12 @@
1
+ .sidebar{
2
+ min-width:292px;
3
+ display:flex;
4
+ flex-direction:column;
5
+ gap:12px;
6
+ }
7
+
8
+ .subheader{
9
+ display:flex;
10
+ align-items:center;
11
+ justify-content:space-between;
12
+ }
@@ -0,0 +1,7 @@
1
+ import { TreeNodeProps } from '@snack-uikit/tree';
2
+ /**
3
+ * Рекурсивно собирает все ID узлов дерева для их разворачивания
4
+ * @param nodes - Массив узлов дерева
5
+ * @returns Массив ID всех узлов (включая вложенные)
6
+ */
7
+ export declare const getExpandedNodes: (nodes: TreeNodeProps[]) => string[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Рекурсивно собирает все ID узлов дерева для их разворачивания
3
+ * @param nodes - Массив узлов дерева
4
+ * @returns Массив ID всех узлов (включая вложенные)
5
+ */
6
+ export const getExpandedNodes = (nodes) => {
7
+ if (!nodes || nodes.length === 0) {
8
+ return [];
9
+ }
10
+ const ids = [];
11
+ nodes.forEach(el => {
12
+ const children = el.nested ? getExpandedNodes(el.nested) : [];
13
+ ids.push(el.id, ...children);
14
+ });
15
+ return ids;
16
+ };
@@ -0,0 +1,2 @@
1
+ export { Menu } from './Menu/Menu';
2
+ export { ConditionalPopover } from './ConditionalPopover/ConditionalPopover';
@@ -0,0 +1,2 @@
1
+ export { Menu } from './Menu/Menu';
2
+ export { ConditionalPopover } from './ConditionalPopover/ConditionalPopover';
@@ -0,0 +1 @@
1
+ export * from './TreeNavigation';
@@ -0,0 +1 @@
1
+ export * from './TreeNavigation';
@@ -0,0 +1,82 @@
1
+ .root{
2
+ display:flex;
3
+ flex-direction:column;
4
+ height:100%;
5
+ width:100%;
6
+ }
7
+
8
+ .header{
9
+ width:100%;
10
+ display:flex;
11
+ justify-content:space-between;
12
+ align-items:flex-start;
13
+ gap:40px;
14
+ padding:8px 24px;
15
+ background-color:var(--sys-neutral-background, #eeeff3);
16
+ border-bottom:1px solid var(--sys-neutral-decor-default, #dde0ea);
17
+ box-sizing:border-box;
18
+ }
19
+
20
+ .description{
21
+ color:var(--sys-neutral-text-support, #6d707f);
22
+ }
23
+
24
+ .body{
25
+ display:flex;
26
+ gap:16px;
27
+ padding:0 24px;
28
+ box-sizing:border-box;
29
+ width:100%;
30
+ }
31
+
32
+ .sidebar{
33
+ border-radius:12px;
34
+ padding:16px;
35
+ height:-moz-max-content;
36
+ height:max-content;
37
+ background:var(--sys-neutral-background1-level, #fdfdfd);
38
+ }
39
+
40
+ .main{
41
+ padding-top:16px;
42
+ width:100%;
43
+ box-sizing:border-box;
44
+ }
45
+
46
+ .titleWrapper{
47
+ display:flex;
48
+ align-items:center;
49
+ flex-wrap:wrap;
50
+ -moz-column-gap:16px;
51
+ column-gap:16px;
52
+ row-gap:8px;
53
+ }
54
+
55
+ .titleInner{
56
+ display:flex;
57
+ gap:8px;
58
+ }
59
+
60
+ .title{
61
+ box-sizing:border-box;
62
+ max-width:100%;
63
+ margin:0;
64
+ padding:0;
65
+ overflow-wrap:break-word;
66
+ }
67
+
68
+ .innerElement{
69
+ display:flex;
70
+ align-items:center;
71
+ justify-content:center;
72
+ height:var(--sans-title-l-line-height, 28px);
73
+ }
74
+
75
+ .icon{
76
+ display:flex;
77
+ align-items:center;
78
+ justify-content:center;
79
+ width:24px;
80
+ height:24px;
81
+ flex-shrink:0;
82
+ }
@@ -4,3 +4,4 @@ export * from './PageLoading';
4
4
  export * from './PageServices';
5
5
  export * from './DefaultSubHeader';
6
6
  export * from './PageSidebar';
7
+ export * from './TreeNavigation';
@@ -4,3 +4,4 @@ export * from './PageLoading';
4
4
  export * from './PageServices';
5
5
  export * from './DefaultSubHeader';
6
6
  export * from './PageSidebar';
7
+ export * from './TreeNavigation';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloud-ru/uikit-product-page-layout",
3
3
  "title": "Page Layout",
4
- "version": "0.21.5",
4
+ "version": "0.22.0",
5
5
  "sideEffects": [
6
6
  "*.css",
7
7
  "*.woff",
@@ -30,22 +30,24 @@
30
30
  "name": "Akhremenko Grigorii",
31
31
  "url": "https://github.com/AGrigorii"
32
32
  },
33
- "contributors": [],
34
33
  "license": "Apache-2.0",
35
34
  "publishConfig": {
36
35
  "access": "public"
37
36
  },
38
37
  "scripts": {},
39
38
  "dependencies": {
40
- "@cloud-ru/uikit-product-copy-line": "0.7.65",
41
- "@cloud-ru/uikit-product-icons": "15.1.3",
42
- "@cloud-ru/uikit-product-utils": "7.0.2",
39
+ "@cloud-ru/uikit-product-copy-line": "0.7.66",
40
+ "@cloud-ru/uikit-product-icons": "15.1.4",
41
+ "@cloud-ru/uikit-product-utils": "7.0.3",
43
42
  "@snack-uikit/button": "0.19.13",
44
43
  "@snack-uikit/icon-predefined": "0.7.3",
45
44
  "@snack-uikit/list": "0.26.0",
46
45
  "@snack-uikit/loaders": "0.9.4",
46
+ "@snack-uikit/popover": "0.10.3",
47
47
  "@snack-uikit/search": "0.12.13",
48
+ "@snack-uikit/status": "0.10.4",
48
49
  "@snack-uikit/tooltip": "0.16.2",
50
+ "@snack-uikit/tree": "0.9.30",
49
51
  "@snack-uikit/truncate-string": "0.6.9",
50
52
  "@snack-uikit/typography": "0.8.4",
51
53
  "@snack-uikit/utils": "3.7.0",
@@ -55,5 +57,5 @@
55
57
  "peerDependencies": {
56
58
  "@cloud-ru/uikit-product-locale": "*"
57
59
  },
58
- "gitHead": "bf479ecf7238ef20b78f20acaef439efa535d1a1"
60
+ "gitHead": "0c7a2375e56560d287c95b9ef47c5b424cb004d6"
59
61
  }
@@ -0,0 +1,128 @@
1
+ import cn from 'classnames';
2
+ import { ReactNode, useMemo } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+
5
+ import { BurgerSVG, CloseSVG } from '@cloud-ru/uikit-product-icons';
6
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
7
+ import { ButtonSimple } from '@snack-uikit/button';
8
+ import { Status, StatusProps } from '@snack-uikit/status';
9
+ import { TreeNodeProps } from '@snack-uikit/tree';
10
+ import { Typography } from '@snack-uikit/typography';
11
+
12
+ import { ConditionalPopover, Menu } from './helper-components';
13
+ import styles from './styles.module.scss';
14
+
15
+ export type TreeNavigationProps = {
16
+ header: {
17
+ /** Текст заголовка */
18
+ title: string;
19
+ /** Иконка */
20
+ icon?: ReactNode;
21
+ /** Текст описания */
22
+ description?: string;
23
+ /** Статус (цвет, иконка и т.п.) – любой тип, который принимает ваш <Status/> */
24
+ status?: StatusProps;
25
+ /** Раздел для действий */
26
+ actions?: ReactNode;
27
+ };
28
+ menu: {
29
+ /** Заголовок меню */
30
+ menuTitle?: string;
31
+ /** Данные для дерева меню */
32
+ items: TreeNodeProps[];
33
+ /** Управляемый режим: если передан, меню открывается как popover. */
34
+ isMenuOpen?: boolean;
35
+ /** Колбэк, вызываемый при попытке изменить состояние меню.
36
+ * В контролируемом режиме обязателен, в неконтролируемом – опционален.
37
+ */
38
+ onMenuToggle?: (open: boolean) => void;
39
+ /** Открывать меню по умолчанию */
40
+ defaultMenuOpened?: boolean;
41
+ /** Позляет отключить кнопку "Свернуть все"*/
42
+ enableShrinkMenuButton?: boolean;
43
+ /** Открывать пункты меню по умолчанию */
44
+ withDefaultOpenedMenuList?: boolean;
45
+ };
46
+ /** Контентная часть страницы */
47
+ content: ReactNode;
48
+ /** Вариант отображения */
49
+ mode: 'popover' | 'aside';
50
+ /** Класс для контейнера контентной части */
51
+ contentClassName?: string;
52
+ };
53
+
54
+ export function TreeNavigation({
55
+ header: { title, icon, description, status, actions },
56
+ menu: {
57
+ menuTitle,
58
+ items,
59
+ enableShrinkMenuButton,
60
+ withDefaultOpenedMenuList,
61
+ isMenuOpen,
62
+ defaultMenuOpened,
63
+ onMenuToggle,
64
+ },
65
+ content,
66
+ mode,
67
+ contentClassName,
68
+ }: TreeNavigationProps) {
69
+ const [open, setOpen] = useUncontrolledProp(isMenuOpen, defaultMenuOpened, onMenuToggle);
70
+ const { t } = useLocale('PageLayout');
71
+ const menu = useMemo(
72
+ () => (
73
+ <Menu
74
+ menuItems={items}
75
+ menuTitle={menuTitle}
76
+ enableShrinkMenuButton={enableShrinkMenuButton}
77
+ withDefaultOpenedMenuList={withDefaultOpenedMenuList}
78
+ />
79
+ ),
80
+ [items, menuTitle, enableShrinkMenuButton, withDefaultOpenedMenuList],
81
+ );
82
+
83
+ return (
84
+ <div className={styles.root}>
85
+ <div className={styles.header}>
86
+ <div className={styles.titleWrapper}>
87
+ <div className={styles.titleInner}>
88
+ <ConditionalPopover
89
+ isOpen={Boolean(open)}
90
+ onOpenChange={setOpen}
91
+ tip={menu}
92
+ withPopover={mode === 'popover'}
93
+ >
94
+ <div className={styles.innerElement}>
95
+ <ButtonSimple
96
+ size='xs'
97
+ aria-label={open ? t('TreeNavigation.closeMenu') : t('TreeNavigation.openMenu')}
98
+ icon={open ? <CloseSVG /> : <BurgerSVG />}
99
+ onClick={() => setOpen(!open)}
100
+ />
101
+ </div>
102
+ </ConditionalPopover>
103
+ {icon && (
104
+ <div className={styles.innerElement}>
105
+ <div className={styles.icon}>{icon}</div>
106
+ </div>
107
+ )}
108
+ <Typography.SansTitleL className={styles.title}>{title}</Typography.SansTitleL>
109
+
110
+ {status && (
111
+ <div className={styles.innerElement}>
112
+ <Status {...status} />
113
+ </div>
114
+ )}
115
+ </div>
116
+ {description && <Typography.SansBodyS className={styles.description}>{description}</Typography.SansBodyS>}
117
+ </div>
118
+
119
+ {actions}
120
+ </div>
121
+
122
+ <div className={styles.body}>
123
+ {mode === 'aside' && open && <aside className={styles.sidebar}>{menu}</aside>}
124
+ <div className={cn(styles.main, contentClassName)}>{content}</div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,34 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { Popover } from '@snack-uikit/popover';
4
+
5
+ import styles from './styles.module.scss';
6
+
7
+ type ConditionalPopoverProps = {
8
+ isOpen: boolean;
9
+ onOpenChange: (value: boolean) => void;
10
+ tip: ReactNode;
11
+ withPopover?: boolean;
12
+ children: ReactNode;
13
+ };
14
+
15
+ export function ConditionalPopover({ tip, withPopover, isOpen, onOpenChange, children }: ConditionalPopoverProps) {
16
+ if (withPopover) {
17
+ return (
18
+ <Popover
19
+ className={styles.popover}
20
+ open={isOpen}
21
+ onOpenChange={() => {
22
+ if (!open) onOpenChange(false);
23
+ }}
24
+ tip={tip}
25
+ trigger={'click'}
26
+ placement={'bottom-start'}
27
+ >
28
+ {children}
29
+ </Popover>
30
+ );
31
+ }
32
+
33
+ return children;
34
+ }
@@ -0,0 +1,7 @@
1
+ .popover {
2
+ max-height: 80%;
3
+
4
+ & > div {
5
+ overflow: auto;
6
+ }
7
+ }
@@ -0,0 +1,50 @@
1
+ import { useMemo, useState } from 'react';
2
+
3
+ import { HorizontalMenuCloseSVG } from '@cloud-ru/uikit-product-icons';
4
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
5
+ import { ButtonSimple } from '@snack-uikit/button';
6
+ import { Tree, TreeNodeProps } from '@snack-uikit/tree';
7
+ import { Typography } from '@snack-uikit/typography';
8
+
9
+ import styles from './styles.module.scss';
10
+ import { getExpandedNodes } from './utils';
11
+
12
+ type MenuProps = {
13
+ menuTitle?: string;
14
+ menuItems: TreeNodeProps[];
15
+ enableShrinkMenuButton?: boolean;
16
+ withDefaultOpenedMenuList?: boolean;
17
+ };
18
+
19
+ export function Menu({ menuTitle, menuItems, enableShrinkMenuButton = true, withDefaultOpenedMenuList }: MenuProps) {
20
+ const { t } = useLocale('PageLayout');
21
+ const allExpandedNodes = useMemo(() => getExpandedNodes(menuItems), [menuItems]);
22
+
23
+ const [expandedNodes, setExpandedNodes] = useState<string[]>(withDefaultOpenedMenuList ? allExpandedNodes : []);
24
+
25
+ const isExpanded = expandedNodes.length > 0;
26
+
27
+ const handleExpandAll = () => setExpandedNodes(getExpandedNodes(menuItems));
28
+ const handleCollapseAll = () => setExpandedNodes([]);
29
+
30
+ const showSubheader = Boolean(menuTitle || enableShrinkMenuButton);
31
+
32
+ return (
33
+ <div className={styles.sidebar}>
34
+ {showSubheader && (
35
+ <div className={styles.subheader}>
36
+ <Typography.SansTitleM>{menuTitle}</Typography.SansTitleM>
37
+ {enableShrinkMenuButton && (
38
+ <ButtonSimple
39
+ label={isExpanded ? t('TreeNavigation.collapseAll') : t('TreeNavigation.expandAll')}
40
+ icon={<HorizontalMenuCloseSVG />}
41
+ onClick={isExpanded ? handleCollapseAll : handleExpandAll}
42
+ />
43
+ )}
44
+ </div>
45
+ )}
46
+
47
+ <Tree data={menuItems} selectionMode='single' expandedNodes={expandedNodes} onExpand={setExpandedNodes} />
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,14 @@
1
+ $sidebar-width: 292px; // Общая ширина 324пкс из-за 16пкс paddings
2
+
3
+ .sidebar {
4
+ min-width: $sidebar-width;
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 12px;
8
+ }
9
+
10
+ .subheader {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ }
@@ -0,0 +1,22 @@
1
+ import { TreeNodeProps } from '@snack-uikit/tree';
2
+
3
+ /**
4
+ * Рекурсивно собирает все ID узлов дерева для их разворачивания
5
+ * @param nodes - Массив узлов дерева
6
+ * @returns Массив ID всех узлов (включая вложенные)
7
+ */
8
+ export const getExpandedNodes = (nodes: TreeNodeProps[]): string[] => {
9
+ if (!nodes || nodes.length === 0) {
10
+ return [];
11
+ }
12
+
13
+ const ids: string[] = [];
14
+
15
+ nodes.forEach(el => {
16
+ const children = el.nested ? getExpandedNodes(el.nested) : [];
17
+
18
+ ids.push(el.id, ...children);
19
+ });
20
+
21
+ return ids;
22
+ };
@@ -0,0 +1,2 @@
1
+ export { Menu } from './Menu/Menu';
2
+ export { ConditionalPopover } from './ConditionalPopover/ConditionalPopover';
@@ -0,0 +1 @@
1
+ export * from './TreeNavigation';