@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.
Files changed (49) hide show
  1. package/dist/Layout.d.ts +3 -3
  2. package/dist/Layout.d.ts.map +1 -1
  3. package/dist/Layout.js +7 -1
  4. package/dist/Layout.js.map +1 -1
  5. package/dist/LayoutOverlayManager.d.ts +22 -7
  6. package/dist/LayoutOverlayManager.d.ts.map +1 -1
  7. package/dist/LayoutOverlayManager.js +26 -19
  8. package/dist/LayoutOverlayManager.js.map +1 -1
  9. package/dist/components/Dialog.d.ts +9 -1
  10. package/dist/components/Dialog.d.ts.map +1 -1
  11. package/dist/components/Dialog.js +8 -3
  12. package/dist/components/Dialog.js.map +1 -1
  13. package/dist/components/Menu/MenuContent.d.ts +1 -1
  14. package/dist/components/Menu/MenuContent.d.ts.map +1 -1
  15. package/dist/components/Menu/MenuContent.js +39 -12
  16. package/dist/components/Menu/MenuContent.js.map +1 -1
  17. package/dist/components/Menu/MenuSections.d.ts +1 -1
  18. package/dist/components/Menu/MenuSections.d.ts.map +1 -1
  19. package/dist/components/Menu/MenuSections.js +31 -8
  20. package/dist/components/Menu/MenuSections.js.map +1 -1
  21. package/dist/components/Menu/PageSelector.d.ts +1 -1
  22. package/dist/components/Menu/PageSelector.d.ts.map +1 -1
  23. package/dist/components/Menu/PageSelector.js +9 -3
  24. package/dist/components/Menu/PageSelector.js.map +1 -1
  25. package/dist/components/Menu/types.d.ts +40 -7
  26. package/dist/components/Menu/types.d.ts.map +1 -1
  27. package/dist/components/OverlayContent.d.ts.map +1 -1
  28. package/dist/components/OverlayContent.js +3 -1
  29. package/dist/components/OverlayContent.js.map +1 -1
  30. package/dist/components/SelectionList.d.ts.map +1 -1
  31. package/dist/components/SelectionList.js +5 -1
  32. package/dist/components/SelectionList.js.map +1 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/layout.css +16 -2
  38. package/package.json +2 -2
  39. package/src/Layout.tsx +13 -5
  40. package/src/LayoutOverlayManager.tsx +42 -19
  41. package/src/components/Dialog.tsx +34 -6
  42. package/src/components/Menu/MenuContent.tsx +51 -15
  43. package/src/components/Menu/MenuSections.tsx +47 -11
  44. package/src/components/Menu/PageSelector.tsx +24 -12
  45. package/src/components/Menu/types.ts +43 -7
  46. package/src/components/OverlayContent.tsx +3 -1
  47. package/src/components/SelectionList.tsx +5 -1
  48. package/src/index.ts +2 -0
  49. 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, MenuSectionContent } from './types'
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
- function renderSection(
39
- { icon, label, href, onClick, active, content }: MenuSection,
40
- setOverlayContent: (content: MenuSectionContent | undefined) => void,
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 !!content && (!active || !isMenuContentVisible())
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
- setOverlayContent(content)
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
- export const MenuSections = ({ sections }: Pick<MenuProps, 'sections'>) => {
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
- const [overlayContent, setOverlayContent] = useState<MenuSectionContent | undefined>()
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(() => sections.map(s => renderSection(s, setOverlayContent)), [sections])
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
- {overlayContent && <div><MenuContent {...overlayContent} /></div>}
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
- <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>
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
- <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
- />
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
- export interface ItemGroup {
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
- content?: MenuSectionContent,
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
- export interface MenuProps {
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
@@ -27,11 +27,13 @@ const ContentBox = styled.section`
27
27
  }
28
28
  &.panel {
29
29
  padding: 20px;
30
+ display: flex;
31
+ flex-direction: column;
32
+ flex: 1;
30
33
  }
31
34
  header {
32
35
  display: flex;
33
36
  flex-direction: row;
34
- flex: 1;
35
37
  margin-bottom: 1.25rem;
36
38
  }
37
39
  `
@@ -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: hidden;
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 {