@cloud-ru/uikit-product-page-layout 0.21.3

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 (42) hide show
  1. package/CHANGELOG.md +1391 -0
  2. package/LICENSE +201 -0
  3. package/README.md +8 -0
  4. package/package.json +58 -0
  5. package/src/components/DefaultSubHeader/DefaultSubHeader.tsx +25 -0
  6. package/src/components/DefaultSubHeader/index.ts +1 -0
  7. package/src/components/DefaultSubHeader/styles.module.scss +17 -0
  8. package/src/components/Headline/Headline.tsx +49 -0
  9. package/src/components/Headline/index.ts +1 -0
  10. package/src/components/Headline/styles.module.scss +54 -0
  11. package/src/components/PageCatalog/PageCatalog.tsx +24 -0
  12. package/src/components/PageCatalog/index.ts +1 -0
  13. package/src/components/PageCatalog/styles.module.scss +18 -0
  14. package/src/components/PageForm/PageForm.tsx +136 -0
  15. package/src/components/PageForm/constants.ts +14 -0
  16. package/src/components/PageForm/hooks.tsx +34 -0
  17. package/src/components/PageForm/index.ts +3 -0
  18. package/src/components/PageForm/styles.module.scss +59 -0
  19. package/src/components/PageForm/types.ts +6 -0
  20. package/src/components/PageLoading/PageLoading.tsx +18 -0
  21. package/src/components/PageLoading/index.ts +1 -0
  22. package/src/components/PageLoading/styles.module.scss +14 -0
  23. package/src/components/PageServices/PageServices.tsx +91 -0
  24. package/src/components/PageServices/index.ts +1 -0
  25. package/src/components/PageServices/styles.module.scss +45 -0
  26. package/src/components/PageSidebar/PageSidebar.tsx +133 -0
  27. package/src/components/PageSidebar/contexts.tsx +30 -0
  28. package/src/components/PageSidebar/helperComponents/SidebarSearch/SidebarSearch.tsx +34 -0
  29. package/src/components/PageSidebar/helperComponents/SidebarSearch/index.ts +1 -0
  30. package/src/components/PageSidebar/helperComponents/SidebarSearch/styles.module.scss +28 -0
  31. package/src/components/PageSidebar/helperComponents/SidebarSearchToggle/SidebarSearchToggle.tsx +23 -0
  32. package/src/components/PageSidebar/helperComponents/SidebarSearchToggle/index.ts +1 -0
  33. package/src/components/PageSidebar/helperComponents/SidebarTitle/SidebarTitle.tsx +32 -0
  34. package/src/components/PageSidebar/helperComponents/SidebarTitle/index.ts +1 -0
  35. package/src/components/PageSidebar/helperComponents/SidebarTitle/styles.module.scss +20 -0
  36. package/src/components/PageSidebar/hooks/useItemsCreator.tsx +138 -0
  37. package/src/components/PageSidebar/hooks/useSearchFilter.tsx +57 -0
  38. package/src/components/PageSidebar/index.ts +2 -0
  39. package/src/components/PageSidebar/styles.module.scss +101 -0
  40. package/src/components/PageSidebar/types.ts +41 -0
  41. package/src/components/index.ts +6 -0
  42. package/src/index.ts +1 -0
@@ -0,0 +1,59 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables';
2
+
3
+ $prefix-height: styles-theme-variables.simple-var(styles-theme-variables.$sans-headline-m-line-height);
4
+
5
+ .container {
6
+ padding: styles-theme-variables.$dimension-3m;
7
+ min-width: fit-content;
8
+ display: flex;
9
+ justify-content: center;
10
+ gap: 24px;
11
+ min-height: 100%;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .form {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: 32px;
19
+ width: 800px;
20
+ background-color: styles-theme-variables.$sys-neutral-background1-level;
21
+ padding: 56px;
22
+ flex-shrink: 0;
23
+ border-radius: 16px;
24
+ min-height: 100%;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ .headline {
29
+ position: relative;
30
+ }
31
+
32
+ .footer {
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ }
37
+
38
+ .mainActions {
39
+ display: flex;
40
+ align-items: center;
41
+ margin-left: auto;
42
+ }
43
+
44
+ .sideItems {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 16px;
48
+ width: 304px;
49
+ position: sticky;
50
+ top: 24px;
51
+ align-self: flex-start;
52
+ }
53
+
54
+ .body {
55
+ @include styles-theme-variables.composite-var(styles-theme-variables.$sans-body-m);
56
+ color: styles-theme-variables.$sys-neutral-text-main;
57
+ background-color: styles-theme-variables.$sys-neutral-background1-level;
58
+ flex: 1;
59
+ }
@@ -0,0 +1,6 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+
3
+ import { BUTTON_PRIMARY_VARIANT, BUTTON_SECONDARY_VARIANT } from './constants';
4
+
5
+ export type ButtonPrimaryVariant = ValueOf<typeof BUTTON_PRIMARY_VARIANT>;
6
+ export type ButtonSecondaryVariant = ValueOf<typeof BUTTON_SECONDARY_VARIANT>;
@@ -0,0 +1,18 @@
1
+ import cn from 'classnames';
2
+
3
+ import { extractSupportProps, WithSupportProps } from '@cloud-ru/uikit-product-utils';
4
+ import { Spinner as UISpinner } from '@snack-uikit/loaders';
5
+
6
+ import styles from './styles.module.scss';
7
+
8
+ export type PageLoadingProps = WithSupportProps<{
9
+ className?: string;
10
+ }>;
11
+
12
+ export function PageLoading({ className, ...rest }: PageLoadingProps) {
13
+ return (
14
+ <div className={cn(styles.wrapper, className)} {...extractSupportProps(rest)}>
15
+ <UISpinner size='m' className={styles.spinner} />
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1 @@
1
+ export * from './PageLoading';
@@ -0,0 +1,14 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-loader';
2
+
3
+ .wrapper {
4
+ box-sizing: border-box;
5
+ width: 100%;
6
+ height: 100%;
7
+ color: styles-tokens-loader.$sys-neutral-accent-default;
8
+ }
9
+
10
+ .spinner {
11
+ position: fixed;
12
+ top: calc(50% - styles-tokens-loader.simple-var(styles-tokens-loader.$loader-spiner-m, 'height') / 2);
13
+ left: calc(50% - styles-tokens-loader.simple-var(styles-tokens-loader.$loader-spiner-m, 'width') / 2);
14
+ }
@@ -0,0 +1,91 @@
1
+ import cn from 'classnames';
2
+ import { forwardRef, PropsWithChildren, useEffect, useState } from 'react';
3
+
4
+ import { WithSupportProps } from '@cloud-ru/uikit-product-utils';
5
+ import { extractSupportProps } from '@snack-uikit/utils';
6
+
7
+ import { Headline, HeadlineProps } from '../Headline';
8
+ import { PageSidebar, PageSidebarProps } from '../PageSidebar';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type PageServicesProps = WithSupportProps<
12
+ PropsWithChildren<
13
+ Pick<HeadlineProps, 'title' | 'actions' | 'subHeader' | 'afterHeadline' | 'beforeHeadline' | 'truncateTitle'> & {
14
+ className?: string;
15
+ sidebar?: PageSidebarProps;
16
+ autoHeight?: boolean;
17
+ }
18
+ >
19
+ >;
20
+
21
+ const GLOBAL_CONTAINER_ID = 'single-spa-wrapper';
22
+
23
+ export const PageServices = forwardRef<HTMLDivElement, PageServicesProps>(
24
+ (
25
+ {
26
+ children,
27
+ title,
28
+ actions,
29
+ className,
30
+ sidebar,
31
+ beforeHeadline,
32
+ subHeader,
33
+ afterHeadline,
34
+ truncateTitle,
35
+ autoHeight,
36
+ ...rest
37
+ },
38
+ ref,
39
+ ) => {
40
+ const [height, setHeight] = useState(0);
41
+
42
+ useEffect(() => {
43
+ if (autoHeight) return;
44
+
45
+ const container = document.getElementById(GLOBAL_CONTAINER_ID);
46
+
47
+ if (container) {
48
+ const observer = new ResizeObserver(entities =>
49
+ entities.forEach(entity => {
50
+ if (entity.target === container) {
51
+ const [{ blockSize }] = entity.contentBoxSize;
52
+ setHeight(Math.floor(blockSize));
53
+ }
54
+ }),
55
+ );
56
+
57
+ observer.observe(container);
58
+
59
+ return () => observer.disconnect();
60
+ }
61
+ }, [autoHeight]);
62
+
63
+ return (
64
+ <div
65
+ className={cn(styles.wrapper, className)}
66
+ {...(!autoHeight && { style: { height } })}
67
+ {...extractSupportProps(rest)}
68
+ >
69
+ <div className={styles.tempContainer} ref={ref}>
70
+ <div className={styles.container}>
71
+ <Headline
72
+ title={title}
73
+ actions={actions}
74
+ beforeHeadline={beforeHeadline}
75
+ afterHeadline={afterHeadline}
76
+ subHeader={subHeader}
77
+ truncateTitle={truncateTitle}
78
+ />
79
+
80
+ <div className={styles.childWrapper}>{children}</div>
81
+ </div>
82
+ </div>
83
+ {sidebar && (
84
+ <div className={styles.sidebar}>
85
+ <PageSidebar {...sidebar} />
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ },
91
+ );
@@ -0,0 +1 @@
1
+ export * from './PageServices';
@@ -0,0 +1,45 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables';
2
+
3
+ .wrapper {
4
+ display: flex;
5
+ flex-direction: row-reverse;
6
+ min-width: 100%;
7
+ height: 100%;
8
+ justify-content: flex-end;
9
+ }
10
+
11
+ .sidebar {
12
+ flex-shrink: 0;
13
+ flex-grow: 0;
14
+ max-width: 256px;
15
+ width: fit-content;
16
+ position: relative;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ .tempContainer {
21
+ position: relative;
22
+ box-sizing: border-box;
23
+ min-width: 992px;
24
+ flex-shrink: 1;
25
+ flex-grow: 1;
26
+ overflow-y: auto;
27
+ }
28
+
29
+ .container {
30
+ position: relative;
31
+ min-height: 100%;
32
+ display: flex;
33
+ flex-direction: column;
34
+ padding: styles-theme-variables.$dimension-3m;
35
+ gap: styles-theme-variables.$dimension-2m;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ .childWrapper {
40
+ flex-shrink: 1;
41
+ flex-grow: 1;
42
+ display: flex;
43
+ flex-direction: column;
44
+ box-sizing: border-box;
45
+ }
@@ -0,0 +1,133 @@
1
+ import cn from 'classnames';
2
+ import { useCallback, useState } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+
5
+ import { VerticalMenuCloseSVG, VerticalMenuOpenSVG } from '@cloud-ru/uikit-product-icons';
6
+ import { ButtonElevated } from '@snack-uikit/button';
7
+ import { List, ListProps } from '@snack-uikit/list';
8
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
9
+
10
+ import { SearchContextProvider } from './contexts';
11
+ import { SidebarSearch } from './helperComponents/SidebarSearch';
12
+ import { useItemsContent, useTopPinnedContent } from './hooks/useItemsCreator';
13
+ import { useSearchFilter } from './hooks/useSearchFilter';
14
+ import styles from './styles.module.scss';
15
+ import { Documentation, HeaderProps, SidebarItem } from './types';
16
+
17
+ export type PageSidebarProps = WithSupportProps<{
18
+ open?: boolean;
19
+ defaultOpen?: boolean;
20
+ onOpenChanged?(open: boolean): void;
21
+ items: SidebarItem[];
22
+ footerItems?: SidebarItem[];
23
+ header?: HeaderProps;
24
+ selected?: string | number;
25
+ onSelect?(id: string | number): void;
26
+ className?: string;
27
+ documentation?: Documentation;
28
+ pageContainerId?: string;
29
+ hasSearch?: boolean;
30
+ collapse?: ListProps['collapse'];
31
+ }>;
32
+
33
+ function PrivateSideBar({
34
+ open: openProp,
35
+ defaultOpen,
36
+ onOpenChanged,
37
+ className,
38
+ items,
39
+ footerItems = [],
40
+ header,
41
+ selected,
42
+ onSelect,
43
+ hasSearch,
44
+ collapse,
45
+ ...otherProps
46
+ }: PageSidebarProps) {
47
+ const [open, setOpenState] = useUncontrolledProp(openProp, defaultOpen || true, onOpenChanged);
48
+ const [hoverOff, setHoverOff] = useState(false);
49
+
50
+ const toggleOpen = useCallback(
51
+ (newValue: boolean = !open) => {
52
+ if (newValue === open) {
53
+ return;
54
+ }
55
+
56
+ if (!newValue) {
57
+ /* кнопка сворачивания находится внутри сайдбара, а он открывается по ховеру на него,
58
+ поэтому после клика на время отключаем ховер, чтоб дать ему закрыться */
59
+ setHoverOff(true);
60
+ setTimeout(() => setHoverOff(false), 300);
61
+ }
62
+
63
+ setOpenState(newValue);
64
+ },
65
+ [open, setOpenState],
66
+ );
67
+
68
+ const { filteredList, searchOpened, searchValue, searchCollapseState } = useSearchFilter(items);
69
+ const list = useItemsContent(filteredList, onSelect);
70
+ const footerList = useItemsContent(footerItems);
71
+ const { pinTop } = useTopPinnedContent(header, hasSearch);
72
+
73
+ return (
74
+ <div
75
+ // TODO: typescript error
76
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
77
+ // @ts-ignore
78
+ {...extractSupportProps(otherProps)}
79
+ data-collapsed={!open || undefined}
80
+ className={cn(styles.wrapper, className)}
81
+ >
82
+ {!open && (
83
+ <ButtonElevated
84
+ className={styles.expandButton}
85
+ icon={<VerticalMenuOpenSVG />}
86
+ onClick={() => toggleOpen(true)}
87
+ />
88
+ )}
89
+
90
+ <div
91
+ data-collapsed={!open || undefined}
92
+ data-hover-off={hoverOff || undefined}
93
+ data-has-search={hasSearch || undefined}
94
+ className={styles.body}
95
+ >
96
+ <div className={styles.content} data-collapsed={!open || undefined}>
97
+ <div className={styles.list}>
98
+ <List
99
+ selection={{ mode: 'single', value: selected }}
100
+ size='m'
101
+ items={list}
102
+ pinTop={pinTop}
103
+ pinBottom={footerList}
104
+ scroll
105
+ scrollToSelectedItem
106
+ collapse={searchValue ? searchCollapseState : collapse}
107
+ barHideStrategy='leave'
108
+ />
109
+ {hasSearch && searchOpened && (
110
+ <div className={styles.searchWrapper}>
111
+ <SidebarSearch />
112
+ </div>
113
+ )}
114
+ </div>
115
+ <div className={styles.toggler}>
116
+ <ButtonElevated
117
+ icon={open ? <VerticalMenuCloseSVG /> : <VerticalMenuOpenSVG />}
118
+ onClick={() => toggleOpen()}
119
+ />
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export function PageSidebar(props: PageSidebarProps) {
128
+ return (
129
+ <SearchContextProvider>
130
+ <PrivateSideBar {...props} />
131
+ </SearchContextProvider>
132
+ );
133
+ }
@@ -0,0 +1,30 @@
1
+ import { createContext, SetStateAction, useContext, useState } from 'react';
2
+
3
+ type SearchContextValue = {
4
+ searchValue: string;
5
+ setSearchValue(searchValue: string): void;
6
+ searchOpened: boolean;
7
+ setSearchOpened: React.Dispatch<SetStateAction<boolean>>;
8
+ };
9
+
10
+ export const SearchContext = createContext<SearchContextValue>({
11
+ searchValue: '',
12
+ searchOpened: false,
13
+ setSearchValue: () => {},
14
+ setSearchOpened: () => {},
15
+ });
16
+
17
+ export function useSearchContext() {
18
+ return useContext(SearchContext);
19
+ }
20
+
21
+ export function SearchContextProvider({ children }: { children: React.ReactNode }) {
22
+ const [searchValue, setSearchValue] = useState('');
23
+ const [searchOpened, setSearchOpened] = useState(false);
24
+
25
+ return (
26
+ <SearchContext.Provider value={{ searchValue, setSearchValue, searchOpened, setSearchOpened }}>
27
+ {children}
28
+ </SearchContext.Provider>
29
+ );
30
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
4
+ import { Search } from '@snack-uikit/search';
5
+
6
+ import { useSearchContext } from '../../contexts';
7
+ import styles from './styles.module.scss';
8
+
9
+ export function SidebarSearch() {
10
+ const { searchValue, setSearchValue, searchOpened } = useSearchContext();
11
+
12
+ const ref = useRef<HTMLInputElement>(null);
13
+
14
+ const { t } = useLocale('PageLayout');
15
+
16
+ useEffect(() => {
17
+ if (searchOpened) {
18
+ ref?.current?.focus();
19
+ }
20
+ }, [searchOpened]);
21
+
22
+ if (!searchOpened) return null;
23
+
24
+ return (
25
+ <Search
26
+ ref={ref}
27
+ className={styles.searchItem}
28
+ size='m'
29
+ placeholder={t('PageSidebar.searchByServices')}
30
+ value={searchValue}
31
+ onChange={setSearchValue}
32
+ />
33
+ );
34
+ }
@@ -0,0 +1 @@
1
+ export { SidebarSearch } from './SidebarSearch';
@@ -0,0 +1,28 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables.scss';
2
+ @use '@sbercloud/figma-tokens-web/build/scss/components/styles-tokens-element';
3
+
4
+ .searchItem {
5
+ background-color: styles-theme-variables.$sys-neutral-background;
6
+ }
7
+
8
+ .button {
9
+ cursor: pointer;
10
+ position: relative;
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+ background: none;
15
+ border: none;
16
+
17
+ &:focus,
18
+ &:focus-visible {
19
+ @include styles-tokens-element.outline-var(styles-tokens-element.$container-focused-s);
20
+
21
+ border-radius: styles-theme-variables.$dimension-050m;
22
+ outline-color: styles-tokens-element.$sys-available-complementary;
23
+ }
24
+
25
+ &:active {
26
+ outline: none;
27
+ }
28
+ }
@@ -0,0 +1,23 @@
1
+ import { SearchSVG, VerticalMenuRightCloseSVG } from '@cloud-ru/uikit-product-icons';
2
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
3
+ import { ButtonFunction } from '@snack-uikit/button';
4
+ import { Tooltip } from '@snack-uikit/tooltip';
5
+
6
+ import { useSearchContext } from '../../contexts';
7
+
8
+ export function SidebarSearchToggle() {
9
+ const { searchOpened, setSearchValue, setSearchOpened } = useSearchContext();
10
+
11
+ const { t } = useLocale('PageLayout');
12
+
13
+ const toggle = () => {
14
+ setSearchValue('');
15
+ setSearchOpened(prev => !prev);
16
+ };
17
+
18
+ return (
19
+ <Tooltip tip={searchOpened ? t('PageSidebar.closeSearch') : t('PageSidebar.openSearch')}>
20
+ <ButtonFunction size='s' onClick={toggle} icon={searchOpened ? <VerticalMenuRightCloseSVG /> : <SearchSVG />} />
21
+ </Tooltip>
22
+ );
23
+ }
@@ -0,0 +1 @@
1
+ export { SidebarSearchToggle } from './SidebarSearchToggle';
@@ -0,0 +1,32 @@
1
+ import cn from 'classnames';
2
+ import { ReactNode } from 'react';
3
+
4
+ import { IconPredefined } from '@snack-uikit/icon-predefined';
5
+ import { TruncateString } from '@snack-uikit/truncate-string';
6
+ import { Typography } from '@snack-uikit/typography';
7
+
8
+ import { Icon } from '../../types';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type SidebarTitleProps = {
12
+ title: string;
13
+ icon: Icon;
14
+ className?: string;
15
+ afterContent?: ReactNode;
16
+ };
17
+
18
+ export function SidebarTitle({ title, className, icon, afterContent }: SidebarTitleProps) {
19
+ return (
20
+ <div className={cn(className, styles.wrapper)}>
21
+ <div className={styles.icon}>
22
+ <IconPredefined appearance='neutral' size='s' shape='square' icon={icon} />
23
+ </div>
24
+ <div className={styles.title}>
25
+ <Typography.SansLabelL>
26
+ <TruncateString text={title} />
27
+ </Typography.SansLabelL>
28
+ </div>
29
+ {afterContent}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1 @@
1
+ export * from './SidebarTitle';
@@ -0,0 +1,20 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables.scss';
2
+
3
+ .wrapper {
4
+ display: flex;
5
+ flex-direction: row;
6
+ gap: styles-theme-variables.$dimension-1m;
7
+ align-items: center;
8
+ }
9
+
10
+ .icon {
11
+ flex-grow: 0;
12
+ flex-shrink: 0;
13
+ }
14
+
15
+ .title {
16
+ overflow: hidden;
17
+ flex-grow: 1;
18
+ flex-shrink: 1;
19
+ color: styles-theme-variables.$sys-neutral-text-main;
20
+ }