@stack-spot/portal-layout 1.0.1 → 1.0.2
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 +2 -2
- package/dist/Layout.js +1 -1
- package/dist/LayoutOverlayManager.js +6 -6
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Dialog.js +1 -1
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +1 -1
- package/dist/components/OverlayContent.d.ts +1 -1
- package/dist/components/OverlayContent.js +20 -20
- package/dist/components/PortalSwitcher.d.ts +1 -1
- package/dist/components/PortalSwitcher.js +54 -54
- package/dist/components/Toaster.d.ts +2 -2
- package/dist/components/Toaster.js +1 -1
- package/dist/components/UserMenu.d.ts +1 -1
- package/dist/components/UserMenu.js +41 -41
- package/dist/components/error/ErrorBoundary.d.ts +1 -1
- package/dist/components/error/ErrorBoundary.js +1 -1
- package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
- package/dist/components/error/SilentErrorBoundary.js +1 -1
- package/dist/components/menu/MenuContent.d.ts +2 -2
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +125 -132
- package/dist/components/menu/MenuContent.js.map +1 -1
- package/dist/components/menu/MenuSections.d.ts +1 -1
- package/dist/components/menu/MenuSections.js +1 -1
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/PageSelector.d.ts +1 -1
- package/dist/components/menu/PageSelector.js +69 -69
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/components/tour/PortalSwitcherStep.js +1 -1
- package/dist/layout.css +477 -468
- package/dist/toaster.js +1 -1
- package/package.json +3 -2
- package/readme.md +146 -146
- package/src/Layout.tsx +171 -171
- package/src/LayoutOverlayManager.tsx +464 -464
- package/src/components/Dialog.tsx +140 -140
- package/src/components/Header.tsx +62 -62
- package/src/components/OverlayContent.tsx +80 -80
- package/src/components/PortalSwitcher.tsx +161 -161
- package/src/components/Toaster.tsx +95 -95
- package/src/components/UserMenu.tsx +124 -124
- package/src/components/error/ErrorBoundary.tsx +47 -47
- package/src/components/error/ErrorManager.ts +47 -47
- package/src/components/error/SilentErrorBoundary.tsx +64 -64
- package/src/components/menu/MenuContent.tsx +270 -277
- package/src/components/menu/MenuSections.tsx +320 -320
- package/src/components/menu/PageSelector.tsx +164 -164
- package/src/components/menu/constants.ts +2 -2
- package/src/components/menu/types.ts +205 -205
- package/src/components/tour/PortalSwitcherStep.tsx +39 -39
- package/src/components/types.ts +1 -1
- package/src/dictionary.ts +28 -28
- package/src/elements.ts +30 -30
- package/src/errors.ts +11 -11
- package/src/index.ts +14 -14
- package/src/layout.css +477 -468
- package/src/toaster.tsx +153 -153
- package/src/utils.ts +29 -29
- package/tsconfig.json +8 -8
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
import { Button, Flex, IconBox, Text } from '@citric/core'
|
|
2
|
-
import { ArrowRight, CheckCircleFill, Select } from '@citric/icons'
|
|
3
|
-
import { SelectionList } from '@stack-spot/portal-components/SelectionList'
|
|
4
|
-
import { AI, EDP, HUB, Logo } from '@stack-spot/portal-components/svg'
|
|
5
|
-
import { theme } from '@stack-spot/portal-theme'
|
|
6
|
-
import { useTranslate } from '@stack-spot/portal-translate'
|
|
7
|
-
import { ReactNode, useState } from 'react'
|
|
8
|
-
import styled from 'styled-components'
|
|
9
|
-
import { announce } from '../utils'
|
|
10
|
-
import { PortalAcronym } from './types'
|
|
11
|
-
|
|
12
|
-
const Logos: Record<PortalAcronym, ReactNode> = {
|
|
13
|
-
'AI': <AI />,
|
|
14
|
-
'EDP': <EDP />,
|
|
15
|
-
'HUB': <HUB />,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface Portal {
|
|
19
|
-
/**
|
|
20
|
-
* A Stackspot Portal.
|
|
21
|
-
*/
|
|
22
|
-
acronym: PortalAcronym,
|
|
23
|
-
/**
|
|
24
|
-
* The URL to the Stackspot Portal.
|
|
25
|
-
*/
|
|
26
|
-
url: string,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface PortalSwitcherProps {
|
|
30
|
-
/**
|
|
31
|
-
* The Stackspot portals to show in the selector.
|
|
32
|
-
*/
|
|
33
|
-
portals?: Portal[],
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const PortalSwitcherBox = styled(Flex)`
|
|
37
|
-
flex-direction: column;
|
|
38
|
-
align-items: start;
|
|
39
|
-
z-index: 10;
|
|
40
|
-
|
|
41
|
-
.current-portal {
|
|
42
|
-
padding: 8px;
|
|
43
|
-
border-radius: 4px;
|
|
44
|
-
cursor: pointer;
|
|
45
|
-
&:hover {
|
|
46
|
-
background-color: ${theme.color.light[500]};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.selection-list {
|
|
51
|
-
max-width: 360px;
|
|
52
|
-
box-shadow: 4px 4px 48px 0px #000000;
|
|
53
|
-
position: absolute;
|
|
54
|
-
top: 50px;
|
|
55
|
-
|
|
56
|
-
.selection-list-content {
|
|
57
|
-
padding: 8px;
|
|
58
|
-
border-width: 1px;
|
|
59
|
-
border-style: solid;
|
|
60
|
-
border-color: ${theme.color.light['500']};
|
|
61
|
-
|
|
62
|
-
&> ul {
|
|
63
|
-
display: flex;
|
|
64
|
-
flex-direction: column;
|
|
65
|
-
gap: 8px;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.action {
|
|
69
|
-
padding: 16px;
|
|
70
|
-
background-color: ${theme.color.light['400']};
|
|
71
|
-
border-width: 1px;
|
|
72
|
-
border-style: solid;
|
|
73
|
-
border-color: ${theme.color.light['500']};
|
|
74
|
-
border-radius: 4px;
|
|
75
|
-
|
|
76
|
-
&:hover, &:hover a {
|
|
77
|
-
background-color: ${theme.color.light['500']};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
a {
|
|
81
|
-
height: auto;
|
|
82
|
-
transition: unset;
|
|
83
|
-
align-items: flex-start;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
`
|
|
90
|
-
const PORTAL_SWITCHER_ID = 'PortalSwitcher'
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* A selector with different Stackspot portals.
|
|
94
|
-
* Each item contains a logo with a link to the portal.
|
|
95
|
-
* @param props the component Props {@link PortalSwitcherProps}.
|
|
96
|
-
*/
|
|
97
|
-
export const PortalSwitcher = ({ portals = [] }: PortalSwitcherProps) => {
|
|
98
|
-
const [visible, setVisible] = useState<boolean>(false)
|
|
99
|
-
const t = useTranslate(translations)
|
|
100
|
-
const currentPortal = portals?.find(portal => location.href.startsWith(portal.url))
|
|
101
|
-
|
|
102
|
-
return <PortalSwitcherBox>
|
|
103
|
-
{currentPortal ?
|
|
104
|
-
<Button
|
|
105
|
-
className="current-portal"
|
|
106
|
-
appearance="text"
|
|
107
|
-
colorScheme="light"
|
|
108
|
-
aria-controls={PORTAL_SWITCHER_ID}
|
|
109
|
-
aria-expanded={visible}
|
|
110
|
-
aria-label={`${t.portalSwitcher}: ${currentPortal?.acronym} ${t.selected}`}
|
|
111
|
-
onClick={() => {
|
|
112
|
-
setVisible(true)
|
|
113
|
-
announce(`${t.portalSwitcher} ${t.selected}`)
|
|
114
|
-
}}>
|
|
115
|
-
<Flex alignItems="center" className="portal-switcher">
|
|
116
|
-
{Logos[currentPortal.acronym]}
|
|
117
|
-
<IconBox size="xs" ml={3}>
|
|
118
|
-
<Select />
|
|
119
|
-
</IconBox>
|
|
120
|
-
</Flex>
|
|
121
|
-
</Button> :
|
|
122
|
-
<Logo />}
|
|
123
|
-
<SelectionList
|
|
124
|
-
id={PORTAL_SWITCHER_ID}
|
|
125
|
-
items={portals?.map(portal => ({
|
|
126
|
-
label: {
|
|
127
|
-
id: portal.acronym,
|
|
128
|
-
element: <Flex flexDirection="column">
|
|
129
|
-
{Logos[portal.acronym]}
|
|
130
|
-
<Text appearance="microtext1" mt={3} colorScheme="light.700">{t[portal.acronym]}</Text>
|
|
131
|
-
</Flex>,
|
|
132
|
-
},
|
|
133
|
-
target: '_self',
|
|
134
|
-
href: portal.url,
|
|
135
|
-
active: currentPortal?.acronym == portal.acronym,
|
|
136
|
-
iconActive: <CheckCircleFill aria-label={t.selected} />,
|
|
137
|
-
iconRight: portal.acronym !== currentPortal?.acronym ? <ArrowRight /> : undefined,
|
|
138
|
-
}))}
|
|
139
|
-
visible={visible}
|
|
140
|
-
maxHeight="21rem"
|
|
141
|
-
onHide={() => setVisible(false)} />
|
|
142
|
-
</PortalSwitcherBox >
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const translations = {
|
|
147
|
-
en: {
|
|
148
|
-
EDP: 'Efficient and secure solutions from code to production deployment.',
|
|
149
|
-
AI: 'Speed up coding with efficient suggestions and high-quality results.',
|
|
150
|
-
HUB: 'Discover AI Stacks, knowledge sources, and quick commands, all in one streamlined hub.',
|
|
151
|
-
portalSwitcher: 'Portal switcher',
|
|
152
|
-
selected: 'selected',
|
|
153
|
-
},
|
|
154
|
-
pt: {
|
|
155
|
-
EDP: 'Soluções eficientes e seguras do código até a implantação em produção.',
|
|
156
|
-
AI: 'Acelere o desenvolvimento com sugestões eficientes e resultados de alta qualidade.',
|
|
157
|
-
HUB: 'Descubra AI Stacks, knowledge sources e quick commands, tudo em um hub simplificado.',
|
|
158
|
-
portalSwitcher: 'Seletor de portais',
|
|
159
|
-
selected: 'selecionado',
|
|
160
|
-
},
|
|
161
|
-
}
|
|
1
|
+
import { Button, Flex, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ArrowRight, CheckCircleFill, Select } from '@citric/icons'
|
|
3
|
+
import { SelectionList } from '@stack-spot/portal-components/SelectionList'
|
|
4
|
+
import { AI, EDP, HUB, Logo } from '@stack-spot/portal-components/svg'
|
|
5
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
6
|
+
import { useTranslate } from '@stack-spot/portal-translate'
|
|
7
|
+
import { ReactNode, useState } from 'react'
|
|
8
|
+
import styled from 'styled-components'
|
|
9
|
+
import { announce } from '../utils'
|
|
10
|
+
import { PortalAcronym } from './types'
|
|
11
|
+
|
|
12
|
+
const Logos: Record<PortalAcronym, ReactNode> = {
|
|
13
|
+
'AI': <AI />,
|
|
14
|
+
'EDP': <EDP />,
|
|
15
|
+
'HUB': <HUB />,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Portal {
|
|
19
|
+
/**
|
|
20
|
+
* A Stackspot Portal.
|
|
21
|
+
*/
|
|
22
|
+
acronym: PortalAcronym,
|
|
23
|
+
/**
|
|
24
|
+
* The URL to the Stackspot Portal.
|
|
25
|
+
*/
|
|
26
|
+
url: string,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PortalSwitcherProps {
|
|
30
|
+
/**
|
|
31
|
+
* The Stackspot portals to show in the selector.
|
|
32
|
+
*/
|
|
33
|
+
portals?: Portal[],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PortalSwitcherBox = styled(Flex)`
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: start;
|
|
39
|
+
z-index: 10;
|
|
40
|
+
|
|
41
|
+
.current-portal {
|
|
42
|
+
padding: 8px;
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
&:hover {
|
|
46
|
+
background-color: ${theme.color.light[500]};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.selection-list {
|
|
51
|
+
max-width: 360px;
|
|
52
|
+
box-shadow: 4px 4px 48px 0px #000000;
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 50px;
|
|
55
|
+
|
|
56
|
+
.selection-list-content {
|
|
57
|
+
padding: 8px;
|
|
58
|
+
border-width: 1px;
|
|
59
|
+
border-style: solid;
|
|
60
|
+
border-color: ${theme.color.light['500']};
|
|
61
|
+
|
|
62
|
+
&> ul {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.action {
|
|
69
|
+
padding: 16px;
|
|
70
|
+
background-color: ${theme.color.light['400']};
|
|
71
|
+
border-width: 1px;
|
|
72
|
+
border-style: solid;
|
|
73
|
+
border-color: ${theme.color.light['500']};
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
|
|
76
|
+
&:hover, &:hover a {
|
|
77
|
+
background-color: ${theme.color.light['500']};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
a {
|
|
81
|
+
height: auto;
|
|
82
|
+
transition: unset;
|
|
83
|
+
align-items: flex-start;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
`
|
|
90
|
+
const PORTAL_SWITCHER_ID = 'PortalSwitcher'
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A selector with different Stackspot portals.
|
|
94
|
+
* Each item contains a logo with a link to the portal.
|
|
95
|
+
* @param props the component Props {@link PortalSwitcherProps}.
|
|
96
|
+
*/
|
|
97
|
+
export const PortalSwitcher = ({ portals = [] }: PortalSwitcherProps) => {
|
|
98
|
+
const [visible, setVisible] = useState<boolean>(false)
|
|
99
|
+
const t = useTranslate(translations)
|
|
100
|
+
const currentPortal = portals?.find(portal => location.href.startsWith(portal.url))
|
|
101
|
+
|
|
102
|
+
return <PortalSwitcherBox>
|
|
103
|
+
{currentPortal ?
|
|
104
|
+
<Button
|
|
105
|
+
className="current-portal"
|
|
106
|
+
appearance="text"
|
|
107
|
+
colorScheme="light"
|
|
108
|
+
aria-controls={PORTAL_SWITCHER_ID}
|
|
109
|
+
aria-expanded={visible}
|
|
110
|
+
aria-label={`${t.portalSwitcher}: ${currentPortal?.acronym} ${t.selected}`}
|
|
111
|
+
onClick={() => {
|
|
112
|
+
setVisible(true)
|
|
113
|
+
announce(`${t.portalSwitcher} ${t.selected}`)
|
|
114
|
+
}}>
|
|
115
|
+
<Flex alignItems="center" className="portal-switcher">
|
|
116
|
+
{Logos[currentPortal.acronym]}
|
|
117
|
+
<IconBox size="xs" ml={3}>
|
|
118
|
+
<Select />
|
|
119
|
+
</IconBox>
|
|
120
|
+
</Flex>
|
|
121
|
+
</Button> :
|
|
122
|
+
<Logo />}
|
|
123
|
+
<SelectionList
|
|
124
|
+
id={PORTAL_SWITCHER_ID}
|
|
125
|
+
items={portals?.map(portal => ({
|
|
126
|
+
label: {
|
|
127
|
+
id: portal.acronym,
|
|
128
|
+
element: <Flex flexDirection="column">
|
|
129
|
+
{Logos[portal.acronym]}
|
|
130
|
+
<Text appearance="microtext1" mt={3} colorScheme="light.700">{t[portal.acronym]}</Text>
|
|
131
|
+
</Flex>,
|
|
132
|
+
},
|
|
133
|
+
target: '_self',
|
|
134
|
+
href: portal.url,
|
|
135
|
+
active: currentPortal?.acronym == portal.acronym,
|
|
136
|
+
iconActive: <CheckCircleFill aria-label={t.selected} />,
|
|
137
|
+
iconRight: portal.acronym !== currentPortal?.acronym ? <ArrowRight /> : undefined,
|
|
138
|
+
}))}
|
|
139
|
+
visible={visible}
|
|
140
|
+
maxHeight="21rem"
|
|
141
|
+
onHide={() => setVisible(false)} />
|
|
142
|
+
</PortalSwitcherBox >
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
const translations = {
|
|
147
|
+
en: {
|
|
148
|
+
EDP: 'Efficient and secure solutions from code to production deployment.',
|
|
149
|
+
AI: 'Speed up coding with efficient suggestions and high-quality results.',
|
|
150
|
+
HUB: 'Discover AI Stacks, knowledge sources, and quick commands, all in one streamlined hub.',
|
|
151
|
+
portalSwitcher: 'Portal switcher',
|
|
152
|
+
selected: 'selected',
|
|
153
|
+
},
|
|
154
|
+
pt: {
|
|
155
|
+
EDP: 'Soluções eficientes e seguras do código até a implantação em produção.',
|
|
156
|
+
AI: 'Acelere o desenvolvimento com sugestões eficientes e resultados de alta qualidade.',
|
|
157
|
+
HUB: 'Descubra AI Stacks, knowledge sources e quick commands, tudo em um hub simplificado.',
|
|
158
|
+
portalSwitcher: 'Seletor de portais',
|
|
159
|
+
selected: 'selecionado',
|
|
160
|
+
},
|
|
161
|
+
}
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import { Flex, Text } from '@citric/core'
|
|
2
|
-
import { TimesMini } from '@citric/icons'
|
|
3
|
-
import { IconButton } from '@citric/ui'
|
|
4
|
-
import { useAnchorTag } from '@stack-spot/portal-components/anchor'
|
|
5
|
-
import { useMemo } from 'react'
|
|
6
|
-
import type { CloseButton as DefaultCloseButton } from 'react-toastify'
|
|
7
|
-
import { ToastContainer, toast } from 'react-toastify'
|
|
8
|
-
import 'react-toastify/dist/ReactToastify.css'
|
|
9
|
-
import { useDictionary } from '../dictionary'
|
|
10
|
-
|
|
11
|
-
type CloseButtonProps = Parameters<typeof DefaultCloseButton>[0]
|
|
12
|
-
|
|
13
|
-
export const TOASTER_CLOSE_BTN_CLASS = 'btn-close'
|
|
14
|
-
|
|
15
|
-
const CloseButton = ({ closeToast }: CloseButtonProps) => {
|
|
16
|
-
const t = useDictionary()
|
|
17
|
-
return (
|
|
18
|
-
<IconButton className={TOASTER_CLOSE_BTN_CLASS} onClick={closeToast} title={t.dismiss}>
|
|
19
|
-
<TimesMini />
|
|
20
|
-
</IconButton>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Uses react-toastify to render a Toaster based on the Citric DS.
|
|
26
|
-
*/
|
|
27
|
-
export const Toaster = () => <ToastContainer closeButton={CloseButton} />
|
|
28
|
-
|
|
29
|
-
export interface ToasterAction {
|
|
30
|
-
/**
|
|
31
|
-
* The button's label.
|
|
32
|
-
*/
|
|
33
|
-
label: string,
|
|
34
|
-
/**
|
|
35
|
-
* A function to run once the button is clicked.
|
|
36
|
-
*/
|
|
37
|
-
onClick?: (event: React.MouseEvent) => void,
|
|
38
|
-
/**
|
|
39
|
-
* If this is set, instead of a button, an anchor is rendered with this href.
|
|
40
|
-
*/
|
|
41
|
-
href?: string,
|
|
42
|
-
/**
|
|
43
|
-
* Whether or not to close the toaster once the button is clicked.
|
|
44
|
-
* @default true
|
|
45
|
-
*/
|
|
46
|
-
closeOnClick?: boolean,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ToasterContentProps {
|
|
50
|
-
id: number | string,
|
|
51
|
-
actions?: ToasterAction[],
|
|
52
|
-
onClick?: (event: React.MouseEvent) => void,
|
|
53
|
-
title?: string,
|
|
54
|
-
message: React.ReactNode,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const actionStyle: React.CSSProperties = {
|
|
58
|
-
background: 'transparent',
|
|
59
|
-
border: 'none',
|
|
60
|
-
padding: 0,
|
|
61
|
-
color: 'inherit',
|
|
62
|
-
font: 'inherit',
|
|
63
|
-
fontWeight: 500,
|
|
64
|
-
cursor: 'pointer',
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Renders a toaster with the default layout for toasters.
|
|
69
|
-
*/
|
|
70
|
-
export const ToasterContent = ({ id, message, actions, onClick, title }: ToasterContentProps) => {
|
|
71
|
-
const Link = useAnchorTag()
|
|
72
|
-
const buttons = useMemo(() => actions?.map(
|
|
73
|
-
({ label, href, onClick, closeOnClick = true }) => (
|
|
74
|
-
<Text
|
|
75
|
-
key={label}
|
|
76
|
-
as={href ? Link : 'button'}
|
|
77
|
-
href={href}
|
|
78
|
-
style={actionStyle}
|
|
79
|
-
onClick={(event: React.MouseEvent) => {
|
|
80
|
-
onClick?.(event)
|
|
81
|
-
if (closeOnClick) toast.dismiss(id)
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
{label}
|
|
85
|
-
</Text>
|
|
86
|
-
),
|
|
87
|
-
), [actions])
|
|
88
|
-
return (
|
|
89
|
-
<div onClick={onClick}>
|
|
90
|
-
<h1 style={{ textTransform: 'capitalize' }}>{title}</h1>
|
|
91
|
-
{typeof message === 'string' ? <p>{message}</p> : message}
|
|
92
|
-
{buttons?.length ? <Flex style={{ gap: '12px', marginTop: '12px' }}>{buttons}</Flex> : null}
|
|
93
|
-
</div>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
1
|
+
import { Flex, Text } from '@citric/core'
|
|
2
|
+
import { TimesMini } from '@citric/icons'
|
|
3
|
+
import { IconButton } from '@citric/ui'
|
|
4
|
+
import { useAnchorTag } from '@stack-spot/portal-components/anchor'
|
|
5
|
+
import { useMemo } from 'react'
|
|
6
|
+
import type { CloseButton as DefaultCloseButton } from 'react-toastify'
|
|
7
|
+
import { ToastContainer, toast } from 'react-toastify'
|
|
8
|
+
import 'react-toastify/dist/ReactToastify.css'
|
|
9
|
+
import { useDictionary } from '../dictionary'
|
|
10
|
+
|
|
11
|
+
type CloseButtonProps = Parameters<typeof DefaultCloseButton>[0]
|
|
12
|
+
|
|
13
|
+
export const TOASTER_CLOSE_BTN_CLASS = 'btn-close'
|
|
14
|
+
|
|
15
|
+
const CloseButton = ({ closeToast }: CloseButtonProps) => {
|
|
16
|
+
const t = useDictionary()
|
|
17
|
+
return (
|
|
18
|
+
<IconButton className={TOASTER_CLOSE_BTN_CLASS} onClick={closeToast} title={t.dismiss}>
|
|
19
|
+
<TimesMini />
|
|
20
|
+
</IconButton>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Uses react-toastify to render a Toaster based on the Citric DS.
|
|
26
|
+
*/
|
|
27
|
+
export const Toaster = () => <ToastContainer closeButton={CloseButton} />
|
|
28
|
+
|
|
29
|
+
export interface ToasterAction {
|
|
30
|
+
/**
|
|
31
|
+
* The button's label.
|
|
32
|
+
*/
|
|
33
|
+
label: string,
|
|
34
|
+
/**
|
|
35
|
+
* A function to run once the button is clicked.
|
|
36
|
+
*/
|
|
37
|
+
onClick?: (event: React.MouseEvent) => void,
|
|
38
|
+
/**
|
|
39
|
+
* If this is set, instead of a button, an anchor is rendered with this href.
|
|
40
|
+
*/
|
|
41
|
+
href?: string,
|
|
42
|
+
/**
|
|
43
|
+
* Whether or not to close the toaster once the button is clicked.
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
closeOnClick?: boolean,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToasterContentProps {
|
|
50
|
+
id: number | string,
|
|
51
|
+
actions?: ToasterAction[],
|
|
52
|
+
onClick?: (event: React.MouseEvent) => void,
|
|
53
|
+
title?: string,
|
|
54
|
+
message: React.ReactNode,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const actionStyle: React.CSSProperties = {
|
|
58
|
+
background: 'transparent',
|
|
59
|
+
border: 'none',
|
|
60
|
+
padding: 0,
|
|
61
|
+
color: 'inherit',
|
|
62
|
+
font: 'inherit',
|
|
63
|
+
fontWeight: 500,
|
|
64
|
+
cursor: 'pointer',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Renders a toaster with the default layout for toasters.
|
|
69
|
+
*/
|
|
70
|
+
export const ToasterContent = ({ id, message, actions, onClick, title }: ToasterContentProps) => {
|
|
71
|
+
const Link = useAnchorTag()
|
|
72
|
+
const buttons = useMemo(() => actions?.map(
|
|
73
|
+
({ label, href, onClick, closeOnClick = true }) => (
|
|
74
|
+
<Text
|
|
75
|
+
key={label}
|
|
76
|
+
as={href ? Link : 'button'}
|
|
77
|
+
href={href}
|
|
78
|
+
style={actionStyle}
|
|
79
|
+
onClick={(event: React.MouseEvent) => {
|
|
80
|
+
onClick?.(event)
|
|
81
|
+
if (closeOnClick) toast.dismiss(id)
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{label}
|
|
85
|
+
</Text>
|
|
86
|
+
),
|
|
87
|
+
), [actions])
|
|
88
|
+
return (
|
|
89
|
+
<div onClick={onClick}>
|
|
90
|
+
<h1 style={{ textTransform: 'capitalize' }}>{title}</h1>
|
|
91
|
+
{typeof message === 'string' ? <p>{message}</p> : message}
|
|
92
|
+
{buttons?.length ? <Flex style={{ gap: '12px', marginTop: '12px' }}>{buttons}</Flex> : null}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|