@superdispatch/ui-lab 0.21.11 → 0.21.12

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 (97) hide show
  1. package/.babelrc.js +5 -0
  2. package/.turbo/turbo-version.log +28 -0
  3. package/package.json +37 -12
  4. package/pkg/README.md +10 -0
  5. package/{dist-node → pkg/dist-node}/index.js +2 -4
  6. package/pkg/dist-node/index.js.map +1 -0
  7. package/{dist-src → pkg/dist-src}/alert/Alert.js +2 -4
  8. package/{dist-src → pkg/dist-src}/banner/Banner.js +0 -0
  9. package/{dist-src → pkg/dist-src}/box/Box.js +0 -0
  10. package/{dist-src → pkg/dist-src}/button/Button.js +0 -0
  11. package/{dist-src → pkg/dist-src}/button-area/ButtonArea.js +0 -0
  12. package/{dist-src → pkg/dist-src}/container/Container.js +0 -0
  13. package/{dist-src → pkg/dist-src}/description-item/DescriptionItem.js +0 -0
  14. package/{dist-src → pkg/dist-src}/file-drop-zone/FileDropZone.js +0 -0
  15. package/{dist-src → pkg/dist-src}/file-list-item/FileListItem.js +0 -0
  16. package/{dist-src → pkg/dist-src}/index.js +0 -0
  17. package/{dist-src → pkg/dist-src}/linked-text/LinkedText.js +0 -0
  18. package/{dist-src → pkg/dist-src}/multiline-text/MultilineText.js +0 -0
  19. package/{dist-src → pkg/dist-src}/navbar/Navbar.js +0 -0
  20. package/{dist-src → pkg/dist-src}/navbar/NavbarAccordion.js +0 -0
  21. package/{dist-src → pkg/dist-src}/navbar/NavbarAvatar.js +0 -0
  22. package/{dist-src → pkg/dist-src}/navbar/NavbarBottomBar.js +0 -0
  23. package/{dist-src → pkg/dist-src}/navbar/NavbarContext.js +0 -0
  24. package/{dist-src → pkg/dist-src}/navbar/NavbarItem.js +0 -0
  25. package/{dist-src → pkg/dist-src}/navbar/NavbarList.js +0 -0
  26. package/{dist-src → pkg/dist-src}/navbar/NavbarMenu.js +0 -0
  27. package/{dist-src → pkg/dist-src}/sidebar/Sidebar.js +0 -0
  28. package/{dist-src → pkg/dist-src}/sidebar/SidebarBackButton.js +0 -0
  29. package/{dist-src → pkg/dist-src}/sidebar/SidebarContainer.js +0 -0
  30. package/{dist-src → pkg/dist-src}/sidebar/SidebarContent.js +0 -0
  31. package/{dist-src → pkg/dist-src}/sidebar/SidebarDivider.js +0 -0
  32. package/{dist-src → pkg/dist-src}/sidebar/SidebarMenuItem.js +0 -0
  33. package/{dist-src → pkg/dist-src}/sidebar/SidebarMenuItemAction.js +0 -0
  34. package/{dist-src → pkg/dist-src}/sidebar/SidebarMenuItemAvatar.js +0 -0
  35. package/{dist-src → pkg/dist-src}/sidebar/SidebarMenuItemContext.js +0 -0
  36. package/{dist-src → pkg/dist-src}/sidebar/SidebarSubheader.js +0 -0
  37. package/{dist-src → pkg/dist-src}/text-box/TextBox.js +0 -0
  38. package/{dist-src → pkg/dist-src}/utils/RuleNormalizer.js +0 -0
  39. package/{dist-src → pkg/dist-src}/utils/mergeStyles.js +0 -0
  40. package/{dist-types → pkg/dist-types}/index.d.ts +0 -0
  41. package/{dist-web → pkg/dist-web}/index.js +2 -4
  42. package/pkg/dist-web/index.js.map +1 -0
  43. package/pkg/package.json +34 -0
  44. package/playroom.ts +23 -0
  45. package/src/alert/Alert.stories.tsx +105 -0
  46. package/src/alert/Alert.tsx +108 -0
  47. package/src/banner/Banner.stories.tsx +64 -0
  48. package/src/banner/Banner.tsx +120 -0
  49. package/src/box/Box.stories.tsx +20 -0
  50. package/src/box/Box.tsx +252 -0
  51. package/src/button/Button.stories.tsx +717 -0
  52. package/src/button/Button.tsx +460 -0
  53. package/src/button-area/ButtonArea.stories.tsx +65 -0
  54. package/src/button-area/ButtonArea.tsx +88 -0
  55. package/src/container/Container.tsx +48 -0
  56. package/src/description-item/DescriptionItem.stories.tsx +163 -0
  57. package/src/description-item/DescriptionItem.tsx +104 -0
  58. package/src/file-drop-zone/FileDropZone.stories.tsx +44 -0
  59. package/src/file-drop-zone/FileDropZone.tsx +170 -0
  60. package/src/file-list-item/FileListItem.stories.tsx +37 -0
  61. package/src/file-list-item/FileListItem.tsx +145 -0
  62. package/src/file-list-item/__tests__/FileListItem.spec.tsx +339 -0
  63. package/src/index.spec.ts +43 -0
  64. package/src/index.ts +28 -0
  65. package/src/linked-text/LinkeText.stories.tsx +42 -0
  66. package/src/linked-text/LinkedText.tsx +47 -0
  67. package/src/multiline-text/MultilineText.stories.tsx +30 -0
  68. package/src/multiline-text/MultilineText.ts +16 -0
  69. package/src/navbar/Navbar.stories.tsx +135 -0
  70. package/src/navbar/Navbar.tsx +111 -0
  71. package/src/navbar/NavbarAccordion.tsx +171 -0
  72. package/src/navbar/NavbarAvatar.tsx +51 -0
  73. package/src/navbar/NavbarBottomBar.tsx +135 -0
  74. package/src/navbar/NavbarContext.tsx +23 -0
  75. package/src/navbar/NavbarItem.tsx +119 -0
  76. package/src/navbar/NavbarList.tsx +225 -0
  77. package/src/navbar/NavbarMenu.tsx +102 -0
  78. package/src/sidebar/Sidebar.stories.tsx +363 -0
  79. package/src/sidebar/Sidebar.tsx +73 -0
  80. package/src/sidebar/SidebarBackButton.tsx +33 -0
  81. package/src/sidebar/SidebarContainer.tsx +114 -0
  82. package/src/sidebar/SidebarContent.tsx +119 -0
  83. package/src/sidebar/SidebarDivider.tsx +15 -0
  84. package/src/sidebar/SidebarMenuItem.tsx +211 -0
  85. package/src/sidebar/SidebarMenuItemAction.tsx +27 -0
  86. package/src/sidebar/SidebarMenuItemAvatar.tsx +59 -0
  87. package/src/sidebar/SidebarMenuItemContext.tsx +33 -0
  88. package/src/sidebar/SidebarSubheader.tsx +38 -0
  89. package/src/styled.d.ts +12 -0
  90. package/src/text-box/TextBox.stories.tsx +108 -0
  91. package/src/text-box/TextBox.tsx +229 -0
  92. package/src/utils/RuleNormalizer.ts +24 -0
  93. package/src/utils/mergeStyles.ts +28 -0
  94. package/tsconfig.json +19 -0
  95. package/LICENSE +0 -21
  96. package/dist-node/index.js.map +0 -1
  97. package/dist-web/index.js.map +0 -1
@@ -0,0 +1,73 @@
1
+ import { Color, Column, Columns, useUID } from '@superdispatch/ui';
2
+ import { forwardRef, ReactNode } from 'react';
3
+ import styled from 'styled-components';
4
+ import { TextBox } from '../text-box/TextBox';
5
+
6
+ const SidebarRoot = styled.aside`
7
+ top: 0;
8
+ position: sticky;
9
+ overflow-y: auto;
10
+ overflow-x: hidden;
11
+
12
+ height: 100vh;
13
+ background-color: ${Color.White};
14
+ border-right: 1px solid ${Color.Silver400};
15
+ `;
16
+
17
+ const SidebarHeader = styled.div`
18
+ top: 0;
19
+ z-index: 2;
20
+ position: sticky;
21
+ padding-left: 24px;
22
+ padding-right: 24px;
23
+ padding-bottom: 8px;
24
+ background-color: ${Color.White};
25
+ `;
26
+
27
+ const SidebarTitle = styled.div`
28
+ height: 64px;
29
+ max-height: 64px;
30
+ display: flex;
31
+ align-items: center;
32
+ `;
33
+
34
+ export interface SidebarProps {
35
+ id?: string;
36
+ title?: ReactNode;
37
+ count?: null | number;
38
+ header?: ReactNode;
39
+ children?: ReactNode;
40
+ }
41
+
42
+ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
43
+ ({ title, count, header, children, id: idProp }, ref) => {
44
+ const id = useUID(idProp);
45
+ const titleID = `${id}-title`;
46
+
47
+ return (
48
+ <SidebarRoot id={id} ref={ref}>
49
+ <SidebarHeader>
50
+ <SidebarTitle>
51
+ <Columns space="xsmall" align="center">
52
+ <Column width="fluid">
53
+ <TextBox variant="heading-2" noWrap={true} id={titleID}>
54
+ {title}
55
+ </TextBox>
56
+ </Column>
57
+
58
+ {!!count && (
59
+ <Column width="content">
60
+ <TextBox color="secondary">{count}</TextBox>
61
+ </Column>
62
+ )}
63
+ </Columns>
64
+ </SidebarTitle>
65
+
66
+ {header}
67
+ </SidebarHeader>
68
+
69
+ <div aria-labelledby={titleID}>{children}</div>
70
+ </SidebarRoot>
71
+ );
72
+ },
73
+ );
@@ -0,0 +1,33 @@
1
+ import { IconButton, IconButtonProps } from '@material-ui/core';
2
+ import { ArrowBack } from '@material-ui/icons';
3
+ import { useCollapseBreakpoint } from '@superdispatch/ui';
4
+ import { ReactElement } from 'react';
5
+ import { useSidebarContext } from './SidebarContainer';
6
+
7
+ export function SidebarBackButton({
8
+ onClick,
9
+ children = <ArrowBack fontSize="small" />,
10
+ ...props
11
+ }: IconButtonProps): ReactElement | null {
12
+ const isCollapsed = useCollapseBreakpoint('sm');
13
+ const { openSidebar } = useSidebarContext();
14
+
15
+ if (!isCollapsed) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <IconButton
21
+ {...props}
22
+ size="small"
23
+ onClick={(event) => {
24
+ onClick?.(event);
25
+ if (!event.isDefaultPrevented()) {
26
+ openSidebar();
27
+ }
28
+ }}
29
+ >
30
+ {children}
31
+ </IconButton>
32
+ );
33
+ }
@@ -0,0 +1,114 @@
1
+ import { useCollapseBreakpoint } from '@superdispatch/ui';
2
+ import {
3
+ createContext,
4
+ forwardRef,
5
+ ReactChild,
6
+ ReactNode,
7
+ useContext,
8
+ useMemo,
9
+ useState,
10
+ } from 'react';
11
+ import styled from 'styled-components';
12
+
13
+ interface SidebarContext {
14
+ openSidebar: () => void;
15
+ openSidebarContent: () => void;
16
+ }
17
+
18
+ const Context = createContext<SidebarContext | null>(null);
19
+
20
+ export function useSidebarContext(): SidebarContext {
21
+ const context = useContext(Context);
22
+
23
+ if (!context) {
24
+ throw new Error('SidebarContext is used outside Provider');
25
+ }
26
+
27
+ return context;
28
+ }
29
+
30
+ const SidebarContainerSidebar = styled.div`
31
+ height: inherit;
32
+ max-height: inherit;
33
+ min-height: inherit;
34
+
35
+ width: 240px;
36
+ `;
37
+
38
+ const SidebarContainerContent = styled.div`
39
+ height: inherit;
40
+ max-height: inherit;
41
+ min-height: inherit;
42
+ `;
43
+
44
+ const SidebarContainerRoot = styled.div`
45
+ display: flex;
46
+
47
+ height: inherit;
48
+ max-height: inherit;
49
+ min-height: inherit;
50
+
51
+ & ${SidebarContainerContent} {
52
+ width: calc(100% - 240px);
53
+ min-width: calc(100% - 240px);
54
+ max-width: calc(100% - 240px);
55
+ }
56
+
57
+ &[data-visibility='sidebar'] ${SidebarContainerSidebar} {
58
+ width: 100%;
59
+ }
60
+
61
+ &[data-visibility='sidebar'] ${SidebarContainerContent} {
62
+ display: none;
63
+ }
64
+
65
+ &[data-visibility='content'] ${SidebarContainerSidebar} {
66
+ display: none;
67
+ }
68
+
69
+ &[data-visibility='content'] ${SidebarContainerContent} {
70
+ width: 100%;
71
+ max-width: unset;
72
+ }
73
+ `;
74
+
75
+ export interface SidebarContainerProps {
76
+ sidebar: ReactChild;
77
+ children?: ReactNode;
78
+ }
79
+
80
+ type SidebarContainerVisibility = 'both' | 'sidebar' | 'content';
81
+
82
+ export const SidebarContainer = forwardRef<
83
+ HTMLDivElement,
84
+ SidebarContainerProps
85
+ >(({ sidebar, children }, ref) => {
86
+ const isCollapsed = useCollapseBreakpoint('sm');
87
+ const [visibilityState, setVisibilityState] =
88
+ useState<SidebarContainerVisibility>('sidebar');
89
+
90
+ const visibility: SidebarContainerVisibility = isCollapsed
91
+ ? visibilityState
92
+ : 'both';
93
+
94
+ const context = useMemo<SidebarContext>(() => {
95
+ return {
96
+ openSidebar: () => {
97
+ setVisibilityState('sidebar');
98
+ },
99
+ openSidebarContent: () => {
100
+ setVisibilityState('content');
101
+ },
102
+ };
103
+ }, []);
104
+
105
+ return (
106
+ <Context.Provider value={context}>
107
+ <SidebarContainerRoot ref={ref} data-visibility={visibility}>
108
+ <SidebarContainerSidebar>{sidebar}</SidebarContainerSidebar>
109
+
110
+ <SidebarContainerContent>{children}</SidebarContainerContent>
111
+ </SidebarContainerRoot>
112
+ </Context.Provider>
113
+ );
114
+ });
@@ -0,0 +1,119 @@
1
+ import { AppBar, Toolbar } from '@material-ui/core';
2
+ import { Column, Columns, Stack, SuperDispatchTheme } from '@superdispatch/ui';
3
+ import {
4
+ MouseEvent,
5
+ ReactElement,
6
+ ReactNode,
7
+ useLayoutEffect,
8
+ useRef,
9
+ } from 'react';
10
+ import styled, { css } from 'styled-components';
11
+ import { Box } from '../box/Box';
12
+ import { TextBox } from '../text-box/TextBox';
13
+ import { SidebarBackButton } from './SidebarBackButton';
14
+ import { useSidebarContext } from './SidebarContainer';
15
+
16
+ const SidebarAppBar = styled(AppBar)(
17
+ ({ theme }: { theme: SuperDispatchTheme }) => {
18
+ return css`
19
+ ${theme.breakpoints.up('sm')} {
20
+ && {
21
+ border-left: transparent;
22
+ }
23
+ }
24
+ `;
25
+ },
26
+ );
27
+
28
+ const ToolbarContent = styled.div`
29
+ flex: 1;
30
+ padding: 16px;
31
+ `;
32
+
33
+ export interface SidebarContentProps {
34
+ dense?: boolean;
35
+ title: ReactNode;
36
+ children: ReactNode;
37
+ action?: ReactNode;
38
+ openOnMount?: boolean;
39
+ closeOnUnmount?: boolean;
40
+ onBack?: (event: MouseEvent<HTMLButtonElement>) => void;
41
+ }
42
+
43
+ export function SidebarContent({
44
+ dense,
45
+ action,
46
+ title,
47
+ children,
48
+ onBack,
49
+ openOnMount,
50
+ closeOnUnmount,
51
+ }: SidebarContentProps): ReactElement {
52
+ const isOpenedOnMount = useRef<boolean>(false);
53
+ const isClosedOnMount = useRef<boolean>(false);
54
+
55
+ const { openSidebarContent, openSidebar } = useSidebarContext();
56
+
57
+ useLayoutEffect(() => {
58
+ if (openOnMount) {
59
+ if (isOpenedOnMount.current) {
60
+ // eslint-disable-next-line no-console
61
+ console.warn(
62
+ '[SidebarContent]: "openOnMount" should not change during lifecycle of the component.',
63
+ );
64
+ } else {
65
+ isOpenedOnMount.current = true;
66
+ openSidebarContent();
67
+ }
68
+ }
69
+ }, [openOnMount, openSidebarContent]);
70
+
71
+ useLayoutEffect(() => {
72
+ return () => {
73
+ if (closeOnUnmount) {
74
+ if (isClosedOnMount.current) {
75
+ // eslint-disable-next-line no-console
76
+ console.warn(
77
+ '[SidebarContent]: "closeOnUnmount" should not change during lifecycle of the component.',
78
+ );
79
+ } else {
80
+ isClosedOnMount.current = true;
81
+ openSidebar();
82
+ }
83
+ }
84
+ };
85
+ }, [openSidebar, closeOnUnmount]);
86
+
87
+ return (
88
+ <Stack space="none">
89
+ <SidebarAppBar>
90
+ <Toolbar disableGutters={true}>
91
+ <ToolbarContent>
92
+ <Columns align={['top', 'center']} space="small">
93
+ <Column width="content">
94
+ <SidebarBackButton onClick={onBack} />
95
+ </Column>
96
+
97
+ <Column>
98
+ <Columns
99
+ space="small"
100
+ collapseBelow="tablet"
101
+ reverse={[true, false]}
102
+ align={['bottom', 'center']}
103
+ >
104
+ <Column>
105
+ <TextBox variant="heading-2">{title}</TextBox>
106
+ </Column>
107
+
108
+ {action && <Column width="content">{action}</Column>}
109
+ </Columns>
110
+ </Column>
111
+ </Columns>
112
+ </ToolbarContent>
113
+ </Toolbar>
114
+ </SidebarAppBar>
115
+
116
+ <Box padding={dense ? 'none' : ['small', 'medium']}>{children}</Box>
117
+ </Stack>
118
+ );
119
+ }
@@ -0,0 +1,15 @@
1
+ import { Divider } from '@material-ui/core';
2
+ import { forwardRef } from 'react';
3
+ import styled from 'styled-components';
4
+
5
+ const SidebarDividerRoot = styled.div`
6
+ padding: 20px 24px;
7
+ `;
8
+
9
+ export const SidebarDivider = forwardRef<HTMLDivElement>((_, ref) => {
10
+ return (
11
+ <SidebarDividerRoot ref={ref}>
12
+ <Divider />
13
+ </SidebarDividerRoot>
14
+ );
15
+ });
@@ -0,0 +1,211 @@
1
+ import { ButtonBase } from '@material-ui/core';
2
+ import { OpenInNew } from '@material-ui/icons';
3
+ import { Color, Column, Columns, Inline, mergeRefs } from '@superdispatch/ui';
4
+ import {
5
+ forwardRef,
6
+ MouseEvent,
7
+ ReactNode,
8
+ useLayoutEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import styled, { css } from 'styled-components';
13
+ import { TextBox } from '../text-box/TextBox';
14
+ import { useSidebarContext } from './SidebarContainer';
15
+ import { SidebarMenuItemContextProvider } from './SidebarMenuItemContext';
16
+
17
+ interface SidebarMenuItemRootProps {
18
+ hasAvatar: boolean;
19
+ }
20
+
21
+ const SidebarMenuItemRoot = styled.div<SidebarMenuItemRootProps>(
22
+ ({ hasAvatar }) => {
23
+ const height = hasAvatar ? 48 : 40;
24
+
25
+ return css`
26
+ position: relative;
27
+
28
+ & > .MuiButtonBase-root {
29
+ width: 100%;
30
+ display: flex;
31
+ justify-content: flex-start;
32
+
33
+ padding-left: 24px;
34
+ padding-right: 24px;
35
+ height: ${height}px;
36
+ max-height: ${height}px;
37
+
38
+ &[aria-current='true'] {
39
+ background-color: ${Color.Silver200};
40
+ box-shadow: inset 4px 0 0 ${Color.Blue300};
41
+ }
42
+ }
43
+ `;
44
+ },
45
+ );
46
+
47
+ const SidebarMenuItemBadge = styled.div`
48
+ font-size: 12px;
49
+ line-height: 16px;
50
+ padding-left: 4px;
51
+ padding-right: 4px;
52
+ border-radius: 100px;
53
+
54
+ color: ${Color.Dark500};
55
+ background-color: ${Color.Silver300};
56
+
57
+ .MuiButtonBase-root[aria-current='true'] & {
58
+ color: ${Color.White};
59
+ background-color: ${Color.Dark450};
60
+ }
61
+ `;
62
+
63
+ const SidebarMenuItemSecondaryAction = styled.div`
64
+ top: 50%;
65
+ right: 24px;
66
+ position: absolute;
67
+ transform: translate3d(0, -50%, 0);
68
+ `;
69
+
70
+ export interface SidebarMenuItemProps {
71
+ selected?: boolean;
72
+ external?: boolean;
73
+ disabled?: boolean;
74
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
75
+
76
+ badge?: null | number;
77
+ action?: ReactNode;
78
+ avatar?: ReactNode;
79
+ children?: ReactNode;
80
+ openContentOnClick?: boolean;
81
+ secondaryActions?: ReactNode;
82
+ }
83
+
84
+ export const SidebarMenuItem = forwardRef<HTMLDivElement, SidebarMenuItemProps>(
85
+ (
86
+ {
87
+ action,
88
+ avatar,
89
+ onClick,
90
+ external,
91
+ children,
92
+ disabled,
93
+ selected,
94
+ secondaryActions,
95
+ badge: badgeProp,
96
+ openContentOnClick,
97
+ },
98
+ ref,
99
+ ) => {
100
+ const [hovered, setHovered] = useState(false);
101
+ const rootRef = useRef<HTMLDivElement>(null);
102
+ const actionsRef = useRef<HTMLDivElement>(null);
103
+ const actionsPlaceholderRef = useRef<HTMLDivElement>(null);
104
+ const { openSidebarContent } = useSidebarContext();
105
+ const { matches: isHoverSupported } = matchMedia('(hover: hover)');
106
+
107
+ useLayoutEffect(() => {
108
+ if (actionsRef.current && actionsPlaceholderRef.current) {
109
+ actionsPlaceholderRef.current.style.width = `${Math.max(
110
+ 0,
111
+ actionsRef.current.offsetWidth - 8,
112
+ )}px`;
113
+ }
114
+ });
115
+
116
+ useLayoutEffect(() => {
117
+ const rootNode = rootRef.current;
118
+
119
+ if (rootNode) {
120
+ if (isHoverSupported) {
121
+ rootNode.addEventListener('mouseenter', () => {
122
+ setHovered(true);
123
+ });
124
+
125
+ rootNode.addEventListener('mouseleave', () => {
126
+ setHovered(false);
127
+ });
128
+ } else {
129
+ setHovered(true);
130
+ }
131
+ }
132
+ }, [isHoverSupported]);
133
+
134
+ const badge =
135
+ !badgeProp || !Number.isFinite(badgeProp)
136
+ ? null
137
+ : badgeProp > 999
138
+ ? '999+'
139
+ : badgeProp;
140
+
141
+ return (
142
+ <SidebarMenuItemRoot ref={mergeRefs(ref, rootRef)} hasAvatar={!!avatar}>
143
+ <ButtonBase
144
+ disabled={disabled}
145
+ aria-current={selected}
146
+ onClick={(event) => {
147
+ onClick?.(event);
148
+ if (!event.isDefaultPrevented() && openContentOnClick) {
149
+ openSidebarContent();
150
+ }
151
+ }}
152
+ >
153
+ <Columns align="center" space="xsmall">
154
+ <Column>
155
+ <Columns align="center" space="xsmall">
156
+ <Column width="fluid">
157
+ <Columns align="center" space="xsmall">
158
+ {!!avatar && (
159
+ <SidebarMenuItemContextProvider
160
+ hovered={hovered}
161
+ disabled={disabled}
162
+ >
163
+ <Column width="content">{avatar}</Column>
164
+ </SidebarMenuItemContextProvider>
165
+ )}
166
+
167
+ <Column width="adaptive">
168
+ <TextBox
169
+ variant={selected ? 'body-semibold' : 'body'}
170
+ noWrap={true}
171
+ >
172
+ {children}
173
+ </TextBox>
174
+ </Column>
175
+
176
+ {external && (
177
+ <Column width="content">
178
+ <OpenInNew color="action" fontSize="small" />
179
+ </Column>
180
+ )}
181
+ </Columns>
182
+ </Column>
183
+ </Columns>
184
+ </Column>
185
+
186
+ {!!badge && (
187
+ <Column width="content">
188
+ <SidebarMenuItemBadge>{badge}</SidebarMenuItemBadge>
189
+ </Column>
190
+ )}
191
+
192
+ {(!!action || !!secondaryActions) && (
193
+ <Column width="content">
194
+ <div ref={actionsPlaceholderRef} />
195
+ </Column>
196
+ )}
197
+ </Columns>
198
+ </ButtonBase>
199
+
200
+ {(!!action || !!secondaryActions) && (
201
+ <SidebarMenuItemSecondaryAction ref={actionsRef}>
202
+ <Inline>
203
+ {hovered && secondaryActions}
204
+ {action}
205
+ </Inline>
206
+ </SidebarMenuItemSecondaryAction>
207
+ )}
208
+ </SidebarMenuItemRoot>
209
+ );
210
+ },
211
+ );
@@ -0,0 +1,27 @@
1
+ import { IconButton, Tooltip, TooltipProps } from '@material-ui/core';
2
+ import { forwardRef, ReactElement } from 'react';
3
+ import styled from 'styled-components';
4
+
5
+ const SidebarMenuItemActionRoot = styled(IconButton)`
6
+ & .MuiSvgIcon-root {
7
+ font-size: 16px;
8
+ }
9
+ `;
10
+
11
+ export interface SidebarMenuItemActionProps
12
+ extends Pick<TooltipProps, 'title' | 'placement'> {
13
+ children: ReactElement;
14
+ }
15
+
16
+ export const SidebarMenuItemAction = forwardRef<
17
+ HTMLButtonElement,
18
+ SidebarMenuItemActionProps
19
+ >(({ title, placement, children }, ref) => {
20
+ return (
21
+ <Tooltip title={title} placement={placement}>
22
+ <SidebarMenuItemActionRoot ref={ref} size="small" edge="end">
23
+ {children}
24
+ </SidebarMenuItemActionRoot>
25
+ </Tooltip>
26
+ );
27
+ });
@@ -0,0 +1,59 @@
1
+ import { Avatar, Checkbox } from '@material-ui/core';
2
+ import { forwardRef, SyntheticEvent, useMemo } from 'react';
3
+ import styled from 'styled-components';
4
+ import { useSidebarMenuItemContext } from './SidebarMenuItemContext';
5
+
6
+ function stopPropagation(event: SyntheticEvent): void {
7
+ event.stopPropagation();
8
+ }
9
+
10
+ const SidebarMenuItemAvatarCheckbox = styled.div`
11
+ margin: -5px;
12
+ `;
13
+
14
+ export interface SidebarMenuItemAvatarProps {
15
+ children: string;
16
+
17
+ value?: boolean;
18
+ onChange?: (checked: boolean) => void;
19
+ }
20
+
21
+ export const SidebarMenuItemAvatar = forwardRef<
22
+ HTMLDivElement,
23
+ SidebarMenuItemAvatarProps
24
+ >(({ children, value, onChange }, ref) => {
25
+ const { hovered, disabled } = useSidebarMenuItemContext();
26
+
27
+ const initials = useMemo(() => {
28
+ const matches = children.match(/\b\w/g) || [];
29
+
30
+ const first = matches.shift() || '';
31
+ const last = matches.pop() || '';
32
+
33
+ return (first + last).toUpperCase();
34
+ }, [children]);
35
+
36
+ if (value === true || (hovered && value != null)) {
37
+ return (
38
+ <SidebarMenuItemAvatarCheckbox>
39
+ <Checkbox
40
+ color="primary"
41
+ checked={value}
42
+ disabled={disabled}
43
+ onClick={stopPropagation}
44
+ onMouseDown={stopPropagation}
45
+ onTouchStart={stopPropagation}
46
+ onChange={(_, checked) => {
47
+ onChange?.(checked);
48
+ }}
49
+ />
50
+ </SidebarMenuItemAvatarCheckbox>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <Avatar ref={ref} aria-hidden={true}>
56
+ {initials}
57
+ </Avatar>
58
+ );
59
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ createContext,
3
+ ReactElement,
4
+ ReactNode,
5
+ useContext,
6
+ useMemo,
7
+ } from 'react';
8
+
9
+ export interface SidebarMenuItemContext {
10
+ hovered?: boolean;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ const context = createContext<SidebarMenuItemContext>({});
15
+
16
+ export function useSidebarMenuItemContext(): SidebarMenuItemContext {
17
+ return useContext(context);
18
+ }
19
+
20
+ export interface SidebarMenuItemContextProviderProps
21
+ extends SidebarMenuItemContext {
22
+ children?: ReactNode;
23
+ }
24
+
25
+ export function SidebarMenuItemContextProvider({
26
+ children,
27
+ hovered = false,
28
+ disabled = false,
29
+ }: SidebarMenuItemContextProviderProps): ReactElement {
30
+ const ctx = useMemo(() => ({ hovered, disabled }), [hovered, disabled]);
31
+
32
+ return <context.Provider value={ctx}>{children}</context.Provider>;
33
+ }