@stack-spot/portal-layout 0.0.1
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/dist/Layout.d.ts +22 -0
- package/dist/Layout.d.ts.map +1 -0
- package/dist/Layout.js +21 -0
- package/dist/Layout.js.map +1 -0
- package/dist/LayoutOverlayManager.d.ts +32 -0
- package/dist/LayoutOverlayManager.d.ts.map +1 -0
- package/dist/LayoutOverlayManager.js +154 -0
- package/dist/LayoutOverlayManager.js.map +1 -0
- package/dist/components/BottomNotification.d.ts +1 -0
- package/dist/components/BottomNotification.d.ts.map +1 -0
- package/dist/components/BottomNotification.js +2 -0
- package/dist/components/BottomNotification.js.map +1 -0
- package/dist/components/BottomPanel.d.ts +1 -0
- package/dist/components/BottomPanel.d.ts.map +1 -0
- package/dist/components/BottomPanel.js +2 -0
- package/dist/components/BottomPanel.js.map +1 -0
- package/dist/components/Dialog.d.ts +21 -0
- package/dist/components/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog.js +19 -0
- package/dist/components/Dialog.js.map +1 -0
- package/dist/components/Header.d.ts +12 -0
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/Logo.d.ts +2 -0
- package/dist/components/Logo.d.ts.map +1 -0
- package/dist/components/Logo.js +4 -0
- package/dist/components/Logo.js.map +1 -0
- package/dist/components/Menu/MenuContent.d.ts +3 -0
- package/dist/components/Menu/MenuContent.d.ts.map +1 -0
- package/dist/components/Menu/MenuContent.js +126 -0
- package/dist/components/Menu/MenuContent.js.map +1 -0
- package/dist/components/Menu/MenuSections.d.ts +3 -0
- package/dist/components/Menu/MenuSections.d.ts.map +1 -0
- package/dist/components/Menu/MenuSections.js +70 -0
- package/dist/components/Menu/MenuSections.js.map +1 -0
- package/dist/components/Menu/PageSelector.d.ts +3 -0
- package/dist/components/Menu/PageSelector.d.ts.map +1 -0
- package/dist/components/Menu/PageSelector.js +87 -0
- package/dist/components/Menu/PageSelector.js.map +1 -0
- package/dist/components/Menu/constants.d.ts +3 -0
- package/dist/components/Menu/constants.d.ts.map +1 -0
- package/dist/components/Menu/constants.js +3 -0
- package/dist/components/Menu/constants.js.map +1 -0
- package/dist/components/Menu/types.d.ts +45 -0
- package/dist/components/Menu/types.d.ts.map +1 -0
- package/dist/components/Menu/types.js +2 -0
- package/dist/components/Menu/types.js.map +1 -0
- package/dist/components/OverlayContent.d.ts +15 -0
- package/dist/components/OverlayContent.d.ts.map +1 -0
- package/dist/components/OverlayContent.js +26 -0
- package/dist/components/OverlayContent.js.map +1 -0
- package/dist/components/SelectionList.d.ts +34 -0
- package/dist/components/SelectionList.d.ts.map +1 -0
- package/dist/components/SelectionList.js +104 -0
- package/dist/components/SelectionList.js.map +1 -0
- package/dist/components/Toaster.d.ts +3 -0
- package/dist/components/Toaster.d.ts.map +1 -0
- package/dist/components/Toaster.js +8 -0
- package/dist/components/Toaster.js.map +1 -0
- package/dist/components/UserMenu.d.ts +9 -0
- package/dist/components/UserMenu.d.ts.map +1 -0
- package/dist/components/UserMenu.js +57 -0
- package/dist/components/UserMenu.js.map +1 -0
- package/dist/components/types.d.ts +6 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +2 -0
- package/dist/components/types.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.css +383 -0
- package/dist/toaster.d.ts +23 -0
- package/dist/toaster.d.ts.map +1 -0
- package/dist/toaster.js +41 -0
- package/dist/toaster.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +8 -0
- package/dist/utils.js.map +1 -0
- package/package.json +39 -0
- package/src/Layout.tsx +68 -0
- package/src/LayoutOverlayManager.tsx +180 -0
- package/src/citric.fix.d.ts +7 -0
- package/src/components/BottomNotification.tsx +0 -0
- package/src/components/BottomPanel.tsx +0 -0
- package/src/components/Dialog.tsx +55 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/Logo.tsx +35 -0
- package/src/components/Menu/MenuContent.tsx +179 -0
- package/src/components/Menu/MenuSections.tsx +105 -0
- package/src/components/Menu/PageSelector.tsx +108 -0
- package/src/components/Menu/constants.ts +2 -0
- package/src/components/Menu/types.ts +53 -0
- package/src/components/OverlayContent.tsx +50 -0
- package/src/components/SelectionList.tsx +200 -0
- package/src/components/Toaster.tsx +12 -0
- package/src/components/UserMenu.tsx +91 -0
- package/src/components/types.ts +5 -0
- package/src/errors.ts +11 -0
- package/src/index.ts +10 -0
- package/src/layout.css +383 -0
- package/src/toaster.tsx +72 -0
- package/src/utils.ts +7 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ArrowRight, Select } from '@citric/icons'
|
|
3
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
4
|
+
import { useMemo, useState } from 'react'
|
|
5
|
+
import { styled } from 'styled-components'
|
|
6
|
+
import { ListAction, SelectionList } from '../SelectionList'
|
|
7
|
+
import { MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
8
|
+
import { Selector } from './types'
|
|
9
|
+
|
|
10
|
+
const SelectorBox = styled.div`
|
|
11
|
+
position: relative;
|
|
12
|
+
margin: ${PADDING}px;
|
|
13
|
+
margin-bottom: 28px;
|
|
14
|
+
|
|
15
|
+
> a {
|
|
16
|
+
display: flex;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
align-items: center;
|
|
19
|
+
border-radius: 0.25rem;
|
|
20
|
+
border: 1px solid ${theme.color.light['500']};
|
|
21
|
+
padding: 8px;
|
|
22
|
+
transition: background-color 0.2s;
|
|
23
|
+
|
|
24
|
+
&:hover {
|
|
25
|
+
background-color: ${theme.color.light['500']};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.label {
|
|
29
|
+
flex: 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.selection-list {
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
box-shadow: none;
|
|
39
|
+
border-radius: 0.25rem;
|
|
40
|
+
|
|
41
|
+
.selection-list-content {
|
|
42
|
+
padding: 8px;
|
|
43
|
+
border-radius: 0.25rem;
|
|
44
|
+
border: none;
|
|
45
|
+
ul {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
li > a {
|
|
53
|
+
border: 1px solid ${theme.color.light['500']};
|
|
54
|
+
background-color: ${theme.color.light['400']};
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
background-color: ${theme.color.light['500']};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.view-all {
|
|
62
|
+
background: ${theme.color.light['500']};
|
|
63
|
+
border-radius: 0.25rem;
|
|
64
|
+
outline: none;
|
|
65
|
+
height: 40px;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
margin-top: 8px;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
export const PageSelector = ({ options, value, button }: Selector) => {
|
|
75
|
+
const [visible, setVisible] = useState(false)
|
|
76
|
+
const { optionsWithIcon, selected } = useMemo(
|
|
77
|
+
() => {
|
|
78
|
+
let selected = options[0]
|
|
79
|
+
const optionsWithIcon = options.map<ListAction>((option) => {
|
|
80
|
+
if (option.key === value) {
|
|
81
|
+
selected = option
|
|
82
|
+
return { ...option, active: true }
|
|
83
|
+
}
|
|
84
|
+
return { ...option, iconRight: <ArrowRight /> }
|
|
85
|
+
})
|
|
86
|
+
return { optionsWithIcon, selected }
|
|
87
|
+
},
|
|
88
|
+
[options, value, button],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<SelectorBox>
|
|
94
|
+
<a onClick={() => setVisible(true)} aria-label={value}>
|
|
95
|
+
{selected?.icon && <IconBox>{selected?.icon}</IconBox>}
|
|
96
|
+
<Text appearance="body2" className="label">{selected?.label ?? button?.label ?? value}</Text>
|
|
97
|
+
<IconBox size="xs"><Select /></IconBox>
|
|
98
|
+
</a>
|
|
99
|
+
|
|
100
|
+
<SelectionList
|
|
101
|
+
visible={visible}
|
|
102
|
+
items={optionsWithIcon}
|
|
103
|
+
onHide={() => setVisible(false)}
|
|
104
|
+
after={button ? <a className="view-all" href={button.href} onClick={button.onClick}>{button.label}</a> : undefined}
|
|
105
|
+
/>
|
|
106
|
+
</SelectorBox>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ReactElement } from 'react'
|
|
2
|
+
import { Action } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface ItemGroup {
|
|
5
|
+
label: string,
|
|
6
|
+
children: MenuItem[],
|
|
7
|
+
open?: boolean,
|
|
8
|
+
hidden?: boolean,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MenuAction extends Action {
|
|
12
|
+
active?: boolean,
|
|
13
|
+
hidden?: boolean,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type MenuItem = ItemGroup | MenuAction
|
|
17
|
+
|
|
18
|
+
export interface MenuButton extends Action {
|
|
19
|
+
icon?: ReactElement,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SelectorItem extends Action {
|
|
23
|
+
key: string,
|
|
24
|
+
icon?: ReactElement,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Selector {
|
|
28
|
+
value?: string,
|
|
29
|
+
options: SelectorItem[],
|
|
30
|
+
button?: Action,
|
|
31
|
+
title?: string,
|
|
32
|
+
subtitle?: string,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MenuSectionContent {
|
|
36
|
+
goBack?: Action,
|
|
37
|
+
title?: string,
|
|
38
|
+
subtitle?: string,
|
|
39
|
+
pageSelector?: Selector,
|
|
40
|
+
options?: MenuItem[],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MenuSection extends Action {
|
|
44
|
+
icon: ReactElement,
|
|
45
|
+
content?: MenuSectionContent,
|
|
46
|
+
active?: boolean,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MenuProps {
|
|
50
|
+
sections: MenuSection[],
|
|
51
|
+
content?: MenuSectionContent,
|
|
52
|
+
compact?: boolean,
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Flex, Text } from '@citric/core'
|
|
2
|
+
import { TimesMini } from '@citric/icons'
|
|
3
|
+
import { IconButton } from '@citric/ui'
|
|
4
|
+
import { WithStyle, listToClass, theme } from '@stack-spot/portal-theme'
|
|
5
|
+
import { ReactNode } from 'react'
|
|
6
|
+
import { styled } from 'styled-components'
|
|
7
|
+
|
|
8
|
+
export interface OverlayContentProps extends WithStyle {
|
|
9
|
+
title: string,
|
|
10
|
+
subtitle?: string,
|
|
11
|
+
children: ReactNode,
|
|
12
|
+
onClose?: () => void,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props extends OverlayContentProps {
|
|
16
|
+
onClose: () => void,
|
|
17
|
+
type: 'modal' | 'panel',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ContentBox = styled.section`
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
border-radius: 1rem;
|
|
24
|
+
background-color: ${theme.color.light['400']};
|
|
25
|
+
&.modal {
|
|
26
|
+
padding: 32px;
|
|
27
|
+
}
|
|
28
|
+
&.panel {
|
|
29
|
+
padding: 20px;
|
|
30
|
+
}
|
|
31
|
+
header {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: row;
|
|
34
|
+
flex: 1;
|
|
35
|
+
margin-bottom: 1.25rem;
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
export const OverlayContent = ({ children, title, subtitle, className, style, onClose, type }: Props) => (
|
|
40
|
+
<ContentBox style={style} className={listToClass([className, type])}>
|
|
41
|
+
<header>
|
|
42
|
+
<Flex flexDirection="column" flex={1}>
|
|
43
|
+
<Text appearance={type === 'modal' ? 'h3' : 'h4'}>{title}</Text>
|
|
44
|
+
{subtitle && <Text appearance="body2" colorScheme="light.700">{subtitle}</Text>}
|
|
45
|
+
</Flex>
|
|
46
|
+
<IconButton onClick={onClose} title="close" aria-label="close"><TimesMini /></IconButton>
|
|
47
|
+
</header>
|
|
48
|
+
{children}
|
|
49
|
+
</ContentBox>
|
|
50
|
+
)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Flex, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ArrowLeft, Check, ChevronRight } from '@citric/icons'
|
|
3
|
+
import { IconButton } from '@citric/ui'
|
|
4
|
+
import { WithStyle, listToClass, theme } from '@stack-spot/portal-theme'
|
|
5
|
+
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import { styled } from 'styled-components'
|
|
7
|
+
import { Action } from './types'
|
|
8
|
+
|
|
9
|
+
interface ItemWithIcon {
|
|
10
|
+
icon?: React.ReactElement,
|
|
11
|
+
iconRight?: React.ReactElement,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ListAction extends ItemWithIcon, Action {
|
|
15
|
+
active?: boolean,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ListGroup {
|
|
19
|
+
type?: 'section' | 'collapsible',
|
|
20
|
+
children: ListItem[],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ListSection extends ListGroup {
|
|
24
|
+
type: 'section',
|
|
25
|
+
label?: string,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ListCollapsible extends ListGroup, ItemWithIcon {
|
|
29
|
+
type?: 'collapsible',
|
|
30
|
+
label: string,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ListItem = ListSection | ListCollapsible | ListAction
|
|
34
|
+
|
|
35
|
+
interface CurrentItemList {
|
|
36
|
+
items: ListItem[],
|
|
37
|
+
label?: string,
|
|
38
|
+
parent?: CurrentItemList,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ANIMATION_DURATION_MS = 300
|
|
42
|
+
|
|
43
|
+
export interface SelectionListProps extends WithStyle {
|
|
44
|
+
visible?: boolean,
|
|
45
|
+
items: ListItem[],
|
|
46
|
+
onHide?: () => void,
|
|
47
|
+
maxHeight?: string,
|
|
48
|
+
before?: ReactElement,
|
|
49
|
+
after?: ReactElement,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SelectionBox = styled.div<{ $maxHeight: string }>`
|
|
53
|
+
max-height: 0;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
transition: max-height ease-in ${ANIMATION_DURATION_MS / 1000}s;
|
|
56
|
+
z-index: 1;
|
|
57
|
+
box-shadow: 4px 4px 48px #000;
|
|
58
|
+
border-radius: 0.5rem;
|
|
59
|
+
|
|
60
|
+
.selection-list-content {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
border-radius: 0.5rem;
|
|
64
|
+
background: ${theme.color.light['500']};
|
|
65
|
+
border-radius: 0.5rem;
|
|
66
|
+
border: 1px solid ${theme.color.light['600']};
|
|
67
|
+
background-color: ${theme.color.light['300']};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.section-title, li > a {
|
|
71
|
+
height: 40px;
|
|
72
|
+
padding: 0 8px;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: row;
|
|
75
|
+
align-items: center;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
li > a {
|
|
79
|
+
gap: 4px;
|
|
80
|
+
transition: background-color 0.2s;
|
|
81
|
+
&:hover {
|
|
82
|
+
background: ${theme.color.light['400']};
|
|
83
|
+
}
|
|
84
|
+
.label {
|
|
85
|
+
flex: 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
li.section {
|
|
90
|
+
border-bottom: 2px solid ${theme.color.light['600']};
|
|
91
|
+
&:last-child {
|
|
92
|
+
border-bottom: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&.visible {
|
|
97
|
+
max-height: ${({ $maxHeight }) => $maxHeight};
|
|
98
|
+
}
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
function renderAction({ label, href, onClick, icon, iconRight, active }: ListAction) {
|
|
102
|
+
return (
|
|
103
|
+
<li key={label} className="action">
|
|
104
|
+
<a href={href} onClick={onClick}>
|
|
105
|
+
{icon && <IconBox>{icon}</IconBox>}
|
|
106
|
+
<Text appearance="body2" className="label">{label}</Text>
|
|
107
|
+
{iconRight && <IconBox>{iconRight}</IconBox>}
|
|
108
|
+
{active && <IconBox><Check /></IconBox>}
|
|
109
|
+
</a>
|
|
110
|
+
</li>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderCollapsible({ label, icon, iconRight, children }: ListCollapsible, setCurrent: (current: CurrentItemList) => void) {
|
|
115
|
+
return (
|
|
116
|
+
<li key={label} className="collapsible">
|
|
117
|
+
<a onClick={() => setCurrent({ items: children, label })}>
|
|
118
|
+
{icon && <IconBox>{icon}</IconBox>}
|
|
119
|
+
<Text appearance="body2" className="label">{label}</Text>
|
|
120
|
+
{iconRight && <IconBox>{iconRight}</IconBox>}
|
|
121
|
+
<IconBox><ChevronRight /></IconBox>
|
|
122
|
+
</a>
|
|
123
|
+
</li>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderSection({ label, children }: ListSection, setCurrent: (current: CurrentItemList) => void) {
|
|
128
|
+
return (
|
|
129
|
+
<li key={label ?? children.map(c => c.label).join('-')} className="section">
|
|
130
|
+
{label && <Text appearance="overheader2" colorScheme="primary" className="section-title">{label}</Text>}
|
|
131
|
+
<ul>{children.map(i => renderItem(i, setCurrent))}</ul>
|
|
132
|
+
</li>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderItem(item: ListItem, setCurrent: (current: CurrentItemList) => void) {
|
|
137
|
+
if ('children' in item) {
|
|
138
|
+
return item.type === 'section' ? renderSection(item, setCurrent) : renderCollapsible(item, setCurrent)
|
|
139
|
+
}
|
|
140
|
+
return renderAction(item)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const SelectionList = ({
|
|
144
|
+
items, className, style, visible = true, maxHeight = '300px', onHide, before, after,
|
|
145
|
+
}: SelectionListProps) => {
|
|
146
|
+
const wrapper = useRef<HTMLDivElement>(null)
|
|
147
|
+
const itemsRef = useRef(items)
|
|
148
|
+
const [current, setCurrent] = useState<CurrentItemList>({ items })
|
|
149
|
+
const listItems = useMemo(
|
|
150
|
+
() => current.items.map(i => renderItem(i, (next: CurrentItemList) => setCurrent({ ...next, parent: current }))),
|
|
151
|
+
[current],
|
|
152
|
+
)
|
|
153
|
+
const hide = useCallback((event: MouseEvent) => {
|
|
154
|
+
const target = (event.target as HTMLElement | null)
|
|
155
|
+
// if the element is not in the DOM anymore, we'll consider the click was inside the selection list
|
|
156
|
+
const isClickInsideSelectionList = !target?.isConnected || wrapper.current?.contains(target)
|
|
157
|
+
const isAction = target?.classList?.contains('action') || !!target?.closest('.action')
|
|
158
|
+
if (!isClickInsideSelectionList || isAction) {
|
|
159
|
+
if (onHide) onHide()
|
|
160
|
+
setTimeout(() => setCurrent({ items: itemsRef.current }), ANIMATION_DURATION_MS)
|
|
161
|
+
document.removeEventListener('click', hide)
|
|
162
|
+
}
|
|
163
|
+
}, [])
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!onHide) return
|
|
167
|
+
if (visible) setTimeout(() => document.addEventListener('click', hide), 50)
|
|
168
|
+
}, [visible])
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
itemsRef.current = items
|
|
172
|
+
if (!wrapper.current?.classList.contains('visible')) setCurrent({ items })
|
|
173
|
+
}, [items])
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<SelectionBox
|
|
177
|
+
ref={wrapper}
|
|
178
|
+
$maxHeight={maxHeight}
|
|
179
|
+
style={style}
|
|
180
|
+
className={listToClass(['selection-list', visible ? 'visible' : undefined, className])}
|
|
181
|
+
>
|
|
182
|
+
<div className="selection-list-content">
|
|
183
|
+
{before}
|
|
184
|
+
{current.parent
|
|
185
|
+
? (
|
|
186
|
+
<Flex mt={5} mb={1} alignItems="center">
|
|
187
|
+
<IconButton onClick={() => setCurrent(current.parent ?? { items })} sx={{ mr: 3 }}>
|
|
188
|
+
<ArrowLeft />
|
|
189
|
+
</IconButton>
|
|
190
|
+
<Text appearance="microtext1">{current.label}</Text>
|
|
191
|
+
</Flex>
|
|
192
|
+
)
|
|
193
|
+
: undefined
|
|
194
|
+
}
|
|
195
|
+
<ul>{listItems}</ul>
|
|
196
|
+
{after}
|
|
197
|
+
</div>
|
|
198
|
+
</SelectionBox>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TimesMini } from '@citric/icons'
|
|
2
|
+
import { IconButton } from '@citric/ui'
|
|
3
|
+
import { CloseButtonProps, ToastContainer } from 'react-toastify'
|
|
4
|
+
import 'react-toastify/dist/ReactToastify.css'
|
|
5
|
+
|
|
6
|
+
const CloseButton = ({ closeToast }: CloseButtonProps) => (
|
|
7
|
+
<IconButton onClick={() => closeToast(null as any)} title="Dismiss">
|
|
8
|
+
<TimesMini />
|
|
9
|
+
</IconButton>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export const Toaster = () => <ToastContainer closeButton={CloseButton} />
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Flex, IconBox, LinkBox, Text } from '@citric/core'
|
|
2
|
+
import { ChevronDown } from '@citric/icons'
|
|
3
|
+
import { Avatar } from '@citric/ui'
|
|
4
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { styled } from 'styled-components'
|
|
7
|
+
import { SelectionList, SelectionListProps } from './SelectionList'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
userName: string,
|
|
11
|
+
email?: string,
|
|
12
|
+
options?: SelectionListProps['items'],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const UserMenuBox = styled.div`
|
|
16
|
+
.user-menu-header {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
padding: 12px;
|
|
22
|
+
border-bottom: 2px solid ${theme.color.light['600']};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.selection-list {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: var(--header-height);
|
|
28
|
+
right: 20px;
|
|
29
|
+
width: 266px;
|
|
30
|
+
|
|
31
|
+
.selection-list-content {
|
|
32
|
+
border: none;
|
|
33
|
+
padding: 16px 16px 8px;
|
|
34
|
+
background-color: ${theme.color.light['400']};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
li {
|
|
38
|
+
margin: 8px 0;
|
|
39
|
+
& > a {
|
|
40
|
+
border-radius: 6px;
|
|
41
|
+
&:hover {
|
|
42
|
+
background: ${theme.color.light['500']};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.username {
|
|
49
|
+
margin: 5px 0 2px 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.chevron {
|
|
53
|
+
transition: transform ease-out 0.3s;
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
const UserMenuHeader = ({ userName, email }: Omit<Props, 'options'>) => (
|
|
58
|
+
<div className="user-menu-header">
|
|
59
|
+
<Avatar size="xs">{userName}</Avatar>
|
|
60
|
+
<Text appearance="body1" className="username">{userName}</Text>
|
|
61
|
+
{email && <Text appearance="microtext1" className="email" colorScheme="light.700">{email}</Text>}
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export const UserMenu = ({ userName, email, options }: Props) => {
|
|
66
|
+
const [visible, setVisible] = useState(false)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<UserMenuBox>
|
|
70
|
+
<LinkBox as="button" onClick={() => setVisible(true)}>
|
|
71
|
+
<Flex alignItems="center">
|
|
72
|
+
<Avatar size="xs">{userName}</Avatar>
|
|
73
|
+
<IconBox colorScheme="inverse" className="chevron" style={visible ? { transform: 'rotate(180deg)' } : undefined}>
|
|
74
|
+
<ChevronDown />
|
|
75
|
+
</IconBox>
|
|
76
|
+
</Flex>
|
|
77
|
+
</LinkBox>
|
|
78
|
+
|
|
79
|
+
{options?.length
|
|
80
|
+
? <SelectionList
|
|
81
|
+
visible={visible}
|
|
82
|
+
before={<UserMenuHeader userName={userName} email={email} />}
|
|
83
|
+
items={options!}
|
|
84
|
+
onHide={() => setVisible(false)}
|
|
85
|
+
maxHeight="600px"
|
|
86
|
+
/>
|
|
87
|
+
: null
|
|
88
|
+
}
|
|
89
|
+
</UserMenuBox>
|
|
90
|
+
)
|
|
91
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class LayoutError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(`Layout error: ${message}`)
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ElementNotFound extends LayoutError {
|
|
8
|
+
constructor(elementName: string, elementId: string) {
|
|
9
|
+
super(`unable to create ${elementName} because no element with id "${elementId}" was found in the view.`)
|
|
10
|
+
}
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Layout } from './Layout'
|
|
2
|
+
export { overlay } from './LayoutOverlayManager'
|
|
3
|
+
export { Header, HeaderProps } from './components/Header'
|
|
4
|
+
export { StackspotLogo } from './components/Logo'
|
|
5
|
+
export { MenuContent } from './components/Menu/MenuContent'
|
|
6
|
+
export { MenuSections } from './components/Menu/MenuSections'
|
|
7
|
+
export * from './components/Menu/types'
|
|
8
|
+
export { ListAction, SelectionList, SelectionListProps } from './components/SelectionList'
|
|
9
|
+
export * from './components/types'
|
|
10
|
+
export * from './errors'
|