@stack-spot/portal-layout 0.0.2 → 0.0.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.
- package/dist/Layout.d.ts +3 -3
- package/dist/Layout.d.ts.map +1 -1
- package/dist/Layout.js +7 -1
- package/dist/Layout.js.map +1 -1
- package/dist/LayoutOverlayManager.d.ts +22 -7
- package/dist/LayoutOverlayManager.d.ts.map +1 -1
- package/dist/LayoutOverlayManager.js +26 -19
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/Dialog.d.ts +9 -1
- package/dist/components/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog.js +8 -3
- package/dist/components/Dialog.js.map +1 -1
- package/dist/components/Menu/MenuContent.d.ts +1 -1
- package/dist/components/Menu/MenuContent.d.ts.map +1 -1
- package/dist/components/Menu/MenuContent.js +39 -12
- package/dist/components/Menu/MenuContent.js.map +1 -1
- package/dist/components/Menu/MenuSections.d.ts +1 -1
- package/dist/components/Menu/MenuSections.d.ts.map +1 -1
- package/dist/components/Menu/MenuSections.js +31 -8
- package/dist/components/Menu/MenuSections.js.map +1 -1
- package/dist/components/Menu/PageSelector.d.ts +1 -1
- package/dist/components/Menu/PageSelector.d.ts.map +1 -1
- package/dist/components/Menu/PageSelector.js +9 -3
- package/dist/components/Menu/PageSelector.js.map +1 -1
- package/dist/components/Menu/types.d.ts +40 -7
- package/dist/components/Menu/types.d.ts.map +1 -1
- package/dist/components/OverlayContent.d.ts.map +1 -1
- package/dist/components/OverlayContent.js +3 -1
- package/dist/components/OverlayContent.js.map +1 -1
- package/dist/components/SelectionList.d.ts.map +1 -1
- package/dist/components/SelectionList.js +5 -1
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layout.css +16 -2
- package/package.json +2 -2
- package/src/Layout.tsx +13 -5
- package/src/LayoutOverlayManager.tsx +42 -19
- package/src/components/Dialog.tsx +34 -6
- package/src/components/Menu/MenuContent.tsx +51 -15
- package/src/components/Menu/MenuSections.tsx +47 -11
- package/src/components/Menu/PageSelector.tsx +24 -12
- package/src/components/Menu/types.ts +43 -7
- package/src/components/OverlayContent.tsx +3 -1
- package/src/components/SelectionList.tsx +5 -1
- package/src/index.ts +2 -0
- package/src/layout.css +16 -2
|
@@ -3,7 +3,7 @@ import { IconBox, Text } from '@citric/core'
|
|
|
3
3
|
import { ChevronLeft, Menu as MenuIcon } from '@citric/icons'
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react'
|
|
5
5
|
import { MenuContent } from './MenuContent'
|
|
6
|
-
import { MenuProps, MenuSection
|
|
6
|
+
import { MenuProps, MenuSection } from './types'
|
|
7
7
|
|
|
8
8
|
const ARROW_HEIGHT = 24
|
|
9
9
|
const HIDE_OVERLAY_DELAY_MS = 400
|
|
@@ -35,19 +35,29 @@ function isMenuContentVisible() {
|
|
|
35
35
|
return !!document.getElementById('layout')?.classList?.contains('menu-content-visible')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const Section = ({
|
|
39
|
+
icon,
|
|
40
|
+
label,
|
|
41
|
+
href,
|
|
42
|
+
onClick,
|
|
43
|
+
active,
|
|
44
|
+
content,
|
|
45
|
+
onOpen,
|
|
46
|
+
setCurrentOverlay,
|
|
47
|
+
id,
|
|
48
|
+
}: MenuSection & { id: number, setCurrentOverlay: (id: number | undefined) => void }) => {
|
|
49
|
+
const contentToRender = typeof content === 'function' ? content() : content
|
|
50
|
+
|
|
42
51
|
function shouldShowOverlay() {
|
|
43
|
-
return !!
|
|
52
|
+
return !!contentToRender && (!active || !isMenuContentVisible())
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
function showOverlayAndFixArrowPosition(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
|
47
56
|
if (!shouldShowOverlay()) return
|
|
57
|
+
onOpen?.()
|
|
48
58
|
const rect = (event.target as HTMLElement)?.getBoundingClientRect()
|
|
49
59
|
const arrow: HTMLElement | null = document.querySelector(`#${MENU_OVERLAY_ID} .arrow`)
|
|
50
|
-
|
|
60
|
+
setCurrentOverlay(id)
|
|
51
61
|
showOverlay()
|
|
52
62
|
if (rect && arrow) {
|
|
53
63
|
arrow.style.top = `${rect.top + rect.height / 2 - ARROW_HEIGHT / 2}px`
|
|
@@ -74,7 +84,14 @@ function renderSection(
|
|
|
74
84
|
)
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
const OverlayRenderer = ({ content }: Pick<MenuSection, 'content'>) => {
|
|
88
|
+
const data = typeof content === 'function' ? content() : content
|
|
89
|
+
return <div><MenuContent {...data} /></div>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const MenuSections = ({ sections, ...props }: MenuProps) => {
|
|
93
|
+
// this is a mock state only used to force an update on the component.
|
|
94
|
+
const [_, setUpdate] = useState(0)
|
|
78
95
|
const toggleMenu = useCallback(() => {
|
|
79
96
|
const layout = document.getElementById('layout')
|
|
80
97
|
if (!layout) return
|
|
@@ -83,10 +100,29 @@ export const MenuSections = ({ sections }: Pick<MenuProps, 'sections'>) => {
|
|
|
83
100
|
} else {
|
|
84
101
|
layout.classList.add('menu-content-visible')
|
|
85
102
|
}
|
|
103
|
+
setUpdate(current => current + 1)
|
|
86
104
|
}, [])
|
|
87
|
-
|
|
105
|
+
// the current overlay showing, when the user hovers the section icon. This is the index of the item in the sections array.
|
|
106
|
+
const [currentOverlay, setCurrentOverlay] = useState<number | undefined>()
|
|
88
107
|
|
|
89
|
-
const sectionItems = useMemo(
|
|
108
|
+
const sectionItems = useMemo(
|
|
109
|
+
() => sections.map((s, i) => <Section key={i} id={i} {...s} setCurrentOverlay={setCurrentOverlay} />),
|
|
110
|
+
[sections],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
/* This function renders the section preview in the overlay in normal circumstances. If the menu is hidden and the section is active,
|
|
114
|
+
instead of rendering the section preview, it will render the actual menu content, which would be invisible otherwise.
|
|
115
|
+
Below, the key is of extreme importance. It ensures React will consider every section content to be an entirely different
|
|
116
|
+
component. Without this, React would see the content changing every time a new section is hovered. Since the content might be a
|
|
117
|
+
hook, this would cause some serious problems. */
|
|
118
|
+
function renderMenuOverlay() {
|
|
119
|
+
if (currentOverlay === undefined) return null
|
|
120
|
+
const shouldRenderMenuContentInstead = !isMenuContentVisible() && sections[currentOverlay].active && !!props.content
|
|
121
|
+
return shouldRenderMenuContentInstead
|
|
122
|
+
? <OverlayRenderer key={'contentKey' in props ? props.contentKey : undefined} content={props.content} />
|
|
123
|
+
: <OverlayRenderer key={currentOverlay} content={sections[currentOverlay].content} />
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
return (
|
|
91
127
|
<>
|
|
92
128
|
<ul>{sectionItems}</ul>
|
|
@@ -97,7 +133,7 @@ export const MenuSections = ({ sections }: Pick<MenuProps, 'sections'>) => {
|
|
|
97
133
|
</IconBox>
|
|
98
134
|
</button>
|
|
99
135
|
<div id="menuContentOverlay" onMouseEnter={showOverlay} onMouseLeave={hideOverlay}>
|
|
100
|
-
{
|
|
136
|
+
{renderMenuOverlay()}
|
|
101
137
|
<div className="arrow"></div>
|
|
102
138
|
</div>
|
|
103
139
|
</>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IconBox, Text } from '@citric/core'
|
|
2
2
|
import { ArrowRight, Select } from '@citric/icons'
|
|
3
|
+
import { LoadingCircular } from '@citric/ui'
|
|
3
4
|
import { theme } from '@stack-spot/portal-theme'
|
|
4
5
|
import { useMemo, useState } from 'react'
|
|
5
6
|
import { styled } from 'styled-components'
|
|
@@ -27,6 +28,9 @@ const SelectorBox = styled.div`
|
|
|
27
28
|
|
|
28
29
|
.label {
|
|
29
30
|
flex: 1;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
text-overflow: ellipsis;
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -71,7 +75,7 @@ const SelectorBox = styled.div`
|
|
|
71
75
|
}
|
|
72
76
|
`
|
|
73
77
|
|
|
74
|
-
export const PageSelector = ({ options, value, button }: Selector) => {
|
|
78
|
+
export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
|
|
75
79
|
const [visible, setVisible] = useState(false)
|
|
76
80
|
const { optionsWithIcon, selected } = useMemo(
|
|
77
81
|
() => {
|
|
@@ -91,18 +95,26 @@ export const PageSelector = ({ options, value, button }: Selector) => {
|
|
|
91
95
|
|
|
92
96
|
return (
|
|
93
97
|
<SelectorBox>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
{loading
|
|
99
|
+
? <LoadingCircular />
|
|
100
|
+
: (
|
|
101
|
+
<>
|
|
102
|
+
{title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
|
|
103
|
+
<a onClick={() => setVisible(true)} aria-label={value}>
|
|
104
|
+
{selected?.icon && <IconBox>{selected?.icon}</IconBox>}
|
|
105
|
+
<Text appearance="body2" className="label">{selected?.label ?? button?.label ?? value}</Text>
|
|
106
|
+
<IconBox size="xs"><Select /></IconBox>
|
|
107
|
+
</a>
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
<SelectionList
|
|
110
|
+
visible={visible}
|
|
111
|
+
items={optionsWithIcon}
|
|
112
|
+
onHide={() => setVisible(false)}
|
|
113
|
+
after={button ? <a className="view-all" href={button.href} onClick={button.onClick}>{button.label}</a> : undefined}
|
|
114
|
+
/>
|
|
115
|
+
</>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
106
118
|
</SelectorBox>
|
|
107
119
|
)
|
|
108
120
|
}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { ReactElement } from 'react'
|
|
2
2
|
import { Action } from '../types'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface BaseMenuItem {
|
|
5
|
+
hidden?: boolean,
|
|
6
|
+
icon?: React.ReactElement,
|
|
7
|
+
/**
|
|
8
|
+
* Whether to wrap overflowing text, just hide or hide and add an ellipsis (...).
|
|
9
|
+
* @default 'wrap'
|
|
10
|
+
*/
|
|
11
|
+
overflow?: 'hidden' | 'wrap' | 'ellipsis',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ItemGroup extends BaseMenuItem {
|
|
5
15
|
label: string,
|
|
6
16
|
children: MenuItem[],
|
|
7
17
|
open?: boolean,
|
|
8
|
-
hidden?: boolean,
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
export interface MenuAction extends Action {
|
|
20
|
+
export interface MenuAction extends Action, BaseMenuItem {
|
|
12
21
|
active?: boolean,
|
|
13
|
-
hidden?: boolean,
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export type MenuItem = ItemGroup | MenuAction
|
|
@@ -30,6 +38,7 @@ export interface Selector {
|
|
|
30
38
|
button?: Action,
|
|
31
39
|
title?: string,
|
|
32
40
|
subtitle?: string,
|
|
41
|
+
loading?: boolean,
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export interface MenuSectionContent {
|
|
@@ -38,16 +47,43 @@ export interface MenuSectionContent {
|
|
|
38
47
|
subtitle?: string,
|
|
39
48
|
pageSelector?: Selector,
|
|
40
49
|
options?: MenuItem[],
|
|
50
|
+
loading?: boolean,
|
|
51
|
+
error?: string,
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface MenuSection extends Action {
|
|
44
55
|
icon: ReactElement,
|
|
45
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The content or a function that creates the content.
|
|
58
|
+
* If this is a function, it will be called only when the section is hovered, i.e. only when the content really needs to be rendered.
|
|
59
|
+
* Tip: this function can be a React Hook.
|
|
60
|
+
*/
|
|
61
|
+
content?: MenuSectionContent | (() => MenuSectionContent),
|
|
46
62
|
active?: boolean,
|
|
63
|
+
onOpen?: () => void,
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
|
|
66
|
+
interface BaseMenuProps {
|
|
50
67
|
sections: MenuSection[],
|
|
51
|
-
content?: MenuSectionContent,
|
|
52
68
|
compact?: boolean,
|
|
53
69
|
}
|
|
70
|
+
|
|
71
|
+
interface MenuPropsWithStaticContent extends BaseMenuProps {
|
|
72
|
+
content?: MenuSectionContent,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface MenuPropsWithDynamicContent extends BaseMenuProps {
|
|
76
|
+
/**
|
|
77
|
+
* The function that creates the content. It will be called only when the content is rendered, i.e. only when the content really needs to
|
|
78
|
+
* be rendered.
|
|
79
|
+
*
|
|
80
|
+
* Tip: this function can be a React Hook.
|
|
81
|
+
*/
|
|
82
|
+
content: MenuSectionContent | (() => MenuSectionContent),
|
|
83
|
+
/**
|
|
84
|
+
* Identifies each content that might be rendered by the menu. This prevents React Hook errors when the content is a React Hook function.
|
|
85
|
+
*/
|
|
86
|
+
contentKey: React.Key,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type MenuProps = MenuPropsWithStaticContent | MenuPropsWithDynamicContent
|
|
@@ -51,7 +51,8 @@ export interface SelectionListProps extends WithStyle {
|
|
|
51
51
|
|
|
52
52
|
const SelectionBox = styled.div<{ $maxHeight: string }>`
|
|
53
53
|
max-height: 0;
|
|
54
|
-
overflow:
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
overflow-x: hidden;
|
|
55
56
|
transition: max-height ease-in ${ANIMATION_DURATION_MS / 1000}s;
|
|
56
57
|
z-index: 1;
|
|
57
58
|
box-shadow: 4px 4px 48px #000;
|
|
@@ -83,6 +84,9 @@ const SelectionBox = styled.div<{ $maxHeight: string }>`
|
|
|
83
84
|
}
|
|
84
85
|
.label {
|
|
85
86
|
flex: 1;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { Layout } from './Layout'
|
|
2
2
|
export { overlay } from './LayoutOverlayManager'
|
|
3
|
+
export { Dialog } from './components/Dialog'
|
|
3
4
|
export { Header, HeaderProps } from './components/Header'
|
|
4
5
|
export { StackspotLogo } from './components/Logo'
|
|
5
6
|
export { MenuContent } from './components/Menu/MenuContent'
|
|
6
7
|
export { MenuSections } from './components/Menu/MenuSections'
|
|
7
8
|
export * from './components/Menu/types'
|
|
9
|
+
export { OverlayContent } from './components/OverlayContent'
|
|
8
10
|
export { ListAction, SelectionList, SelectionListProps } from './components/SelectionList'
|
|
9
11
|
export * from './components/types'
|
|
10
12
|
export * from './errors'
|
package/src/layout.css
CHANGED
|
@@ -298,11 +298,25 @@ body {
|
|
|
298
298
|
display: flex;
|
|
299
299
|
flex-direction: column;
|
|
300
300
|
top: var(--header-height);
|
|
301
|
-
right: -307px;
|
|
302
301
|
bottom: 0;
|
|
303
|
-
width: 307px;
|
|
304
302
|
transition: right var(--right-panel-animation-duration);
|
|
305
303
|
background-color: var(--light-400);
|
|
304
|
+
right: -800px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#rightPanel.small {
|
|
308
|
+
right: -400px;
|
|
309
|
+
width: 400px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#rightPanel.medium {
|
|
313
|
+
right: -600px;
|
|
314
|
+
width: 600px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#rightPanel.large {
|
|
318
|
+
right: -800px;
|
|
319
|
+
width: 800px;
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
#rightPanel.visible {
|