@stack-spot/portal-layout 0.0.52 → 0.0.53
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/SelectionList.d.ts +1 -1
- package/dist/components/SelectionList.js +54 -54
- package/dist/components/SelectionList.js.map +1 -1
- package/dist/components/Toaster.d.ts +1 -1
- 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/ErrorFeedback.d.ts +1 -1
- package/dist/components/error/ErrorFeedback.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 +12 -10
- package/dist/components/menu/MenuContent.d.ts.map +1 -1
- package/dist/components/menu/MenuContent.js +146 -146
- 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 +65 -65
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/components/menu/use-check-text-overflow.js.map +1 -1
- package/dist/layout-context.d.ts +1 -1
- package/dist/layout-context.js +1 -1
- package/dist/layout.css +466 -466
- package/dist/svg/AI.d.ts +1 -1
- package/dist/svg/AI.js +1 -1
- package/dist/svg/EDP.d.ts +1 -1
- package/dist/svg/EDP.js +1 -1
- package/dist/svg/Forbidden.d.ts +1 -1
- package/dist/svg/Forbidden.js +1 -1
- package/dist/svg/HUB.d.ts +1 -1
- package/dist/svg/HUB.js +1 -1
- package/dist/svg/Logo.d.ts +1 -1
- package/dist/svg/Logo.js +1 -1
- package/dist/svg/NotFound.d.ts +1 -1
- package/dist/svg/NotFound.js +1 -1
- package/dist/svg/ServerError.d.ts +1 -1
- package/dist/svg/ServerError.js +1 -1
- package/dist/svg/Unauthenticated.d.ts +1 -1
- package/dist/svg/Unauthenticated.js +1 -1
- package/dist/toaster.js +2 -2
- package/dist/toaster.js.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/Layout.tsx +106 -106
- package/src/LayoutOverlayManager.tsx +273 -273
- package/src/components/Dialog.tsx +93 -93
- package/src/components/Header.tsx +34 -34
- package/src/components/OverlayContent.tsx +58 -58
- package/src/components/PortalSwitcher.tsx +147 -147
- package/src/components/SelectionList.tsx +272 -272
- package/src/components/Toaster.tsx +16 -16
- package/src/components/UserMenu.tsx +111 -111
- package/src/components/error/ErrorBoundary.tsx +38 -38
- package/src/components/error/ErrorFeedback.tsx +114 -114
- package/src/components/error/ErrorManager.ts +31 -31
- package/src/components/error/SilentErrorBoundary.tsx +54 -54
- package/src/components/menu/MenuContent.tsx +296 -296
- package/src/components/menu/MenuSections.tsx +270 -270
- package/src/components/menu/PageSelector.tsx +154 -154
- package/src/components/menu/constants.ts +2 -2
- package/src/components/menu/types.ts +112 -112
- package/src/components/menu/use-check-text-overflow.tsx +26 -26
- package/src/components/menu/use-keyboard-controls.tsx +70 -70
- package/src/components/types.ts +15 -15
- package/src/dictionary.ts +25 -25
- package/src/elements.ts +24 -24
- package/src/errors.ts +11 -11
- package/src/index.ts +17 -17
- package/src/layout-context.tsx +22 -22
- package/src/layout.css +466 -466
- package/src/svg/AI.tsx +37 -37
- package/src/svg/EDP.tsx +35 -35
- package/src/svg/Forbidden.tsx +22 -22
- package/src/svg/HUB.tsx +35 -35
- package/src/svg/Logo.tsx +35 -35
- package/src/svg/NotFound.tsx +16 -16
- package/src/svg/ServerError.tsx +33 -33
- package/src/svg/Unauthenticated.tsx +16 -16
- package/src/toaster.tsx +76 -76
- package/src/utils.ts +114 -114
- package/tsconfig.json +8 -8
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { Button, Flex, Input, Text } from '@citric/core'
|
|
2
|
-
import { ColorSchemeName } from '@stack-spot/portal-theme'
|
|
3
|
-
import { interpolate } from '@stack-spot/portal-translate'
|
|
4
|
-
import { ReactNode, useState } from 'react'
|
|
5
|
-
import { useDictionary } from '../dictionary'
|
|
6
|
-
import { OverlayContent } from './OverlayContent'
|
|
7
|
-
|
|
8
|
-
interface Validation {
|
|
9
|
-
value: string,
|
|
10
|
-
label?: string | ReactNode,
|
|
11
|
-
placeholder?: string,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface DialogOptions {
|
|
15
|
-
title: string,
|
|
16
|
-
subtitle?: string,
|
|
17
|
-
message: ReactNode,
|
|
18
|
-
confirm?: string,
|
|
19
|
-
cancel?: string,
|
|
20
|
-
validation?: false | string | Validation,
|
|
21
|
-
/**
|
|
22
|
-
* @default modal
|
|
23
|
-
*/
|
|
24
|
-
type?: 'modal' | 'panel',
|
|
25
|
-
/**
|
|
26
|
-
* @default right if type is "panel", "right" otherwise.
|
|
27
|
-
*/
|
|
28
|
-
buttonPlacement?: 'left' | 'center' | 'right',
|
|
29
|
-
/**
|
|
30
|
-
* The color of the primary button.
|
|
31
|
-
* @default primary
|
|
32
|
-
*/
|
|
33
|
-
buttonColor?: ColorSchemeName,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface Props extends DialogOptions {
|
|
37
|
-
onConfirm: () => void,
|
|
38
|
-
onCancel: () => void,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const justifyButtons: Record<Required<DialogOptions>['buttonPlacement'], React.CSSProperties['justifyContent']> = {
|
|
42
|
-
center: 'center',
|
|
43
|
-
left: 'start',
|
|
44
|
-
right: 'end',
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const Dialog = ({
|
|
48
|
-
message,
|
|
49
|
-
title,
|
|
50
|
-
subtitle,
|
|
51
|
-
cancel,
|
|
52
|
-
confirm,
|
|
53
|
-
validation,
|
|
54
|
-
onConfirm,
|
|
55
|
-
onCancel,
|
|
56
|
-
type = 'modal',
|
|
57
|
-
buttonPlacement = type === 'panel' ? 'left' : 'right',
|
|
58
|
-
buttonColor = 'primary',
|
|
59
|
-
}: Props,
|
|
60
|
-
) => {
|
|
61
|
-
const t = useDictionary()
|
|
62
|
-
const [enabled, setEnabled] = useState(!validation)
|
|
63
|
-
|
|
64
|
-
function renderValidation() {
|
|
65
|
-
if (!validation) return null
|
|
66
|
-
const value = typeof validation === 'string' ? validation : validation.value
|
|
67
|
-
const label = typeof validation === 'object' && validation.label
|
|
68
|
-
? validation.label
|
|
69
|
-
: interpolate(t.validationLabel, value)
|
|
70
|
-
const placeholder = typeof validation === 'object' ? validation.placeholder : undefined
|
|
71
|
-
return (
|
|
72
|
-
<div style={{ margin: '16px 0' }}>
|
|
73
|
-
<Text>{label}</Text>
|
|
74
|
-
<Input placeholder={placeholder} onChange={e => setEnabled(e.target.value === value)} style={{ marginTop: '10px' }} />
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<OverlayContent title={title} subtitle={subtitle} onClose={onCancel} type={type}>
|
|
81
|
-
<Flex flexDirection="column" flex={1}>
|
|
82
|
-
{message}
|
|
83
|
-
{renderValidation()}
|
|
84
|
-
</Flex>
|
|
85
|
-
{(cancel || confirm) && <Flex gap justifyContent={justifyButtons[buttonPlacement]} alignItems="center" sx={{ mt: 6 }}>
|
|
86
|
-
{cancel && <Button appearance="outlined" colorScheme="inverse" onClick={onCancel}>{cancel}</Button>}
|
|
87
|
-
{confirm && <Button colorScheme={buttonColor} onClick={onConfirm} disabled={!enabled}>
|
|
88
|
-
{confirm}
|
|
89
|
-
</Button>}
|
|
90
|
-
</Flex>}
|
|
91
|
-
</OverlayContent>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
1
|
+
import { Button, Flex, Input, Text } from '@citric/core'
|
|
2
|
+
import { ColorSchemeName } from '@stack-spot/portal-theme'
|
|
3
|
+
import { interpolate } from '@stack-spot/portal-translate'
|
|
4
|
+
import { ReactNode, useState } from 'react'
|
|
5
|
+
import { useDictionary } from '../dictionary'
|
|
6
|
+
import { OverlayContent } from './OverlayContent'
|
|
7
|
+
|
|
8
|
+
interface Validation {
|
|
9
|
+
value: string,
|
|
10
|
+
label?: string | ReactNode,
|
|
11
|
+
placeholder?: string,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DialogOptions {
|
|
15
|
+
title: string,
|
|
16
|
+
subtitle?: string,
|
|
17
|
+
message: ReactNode,
|
|
18
|
+
confirm?: string,
|
|
19
|
+
cancel?: string,
|
|
20
|
+
validation?: false | string | Validation,
|
|
21
|
+
/**
|
|
22
|
+
* @default modal
|
|
23
|
+
*/
|
|
24
|
+
type?: 'modal' | 'panel',
|
|
25
|
+
/**
|
|
26
|
+
* @default right if type is "panel", "right" otherwise.
|
|
27
|
+
*/
|
|
28
|
+
buttonPlacement?: 'left' | 'center' | 'right',
|
|
29
|
+
/**
|
|
30
|
+
* The color of the primary button.
|
|
31
|
+
* @default primary
|
|
32
|
+
*/
|
|
33
|
+
buttonColor?: ColorSchemeName,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Props extends DialogOptions {
|
|
37
|
+
onConfirm: () => void,
|
|
38
|
+
onCancel: () => void,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const justifyButtons: Record<Required<DialogOptions>['buttonPlacement'], React.CSSProperties['justifyContent']> = {
|
|
42
|
+
center: 'center',
|
|
43
|
+
left: 'start',
|
|
44
|
+
right: 'end',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Dialog = ({
|
|
48
|
+
message,
|
|
49
|
+
title,
|
|
50
|
+
subtitle,
|
|
51
|
+
cancel,
|
|
52
|
+
confirm,
|
|
53
|
+
validation,
|
|
54
|
+
onConfirm,
|
|
55
|
+
onCancel,
|
|
56
|
+
type = 'modal',
|
|
57
|
+
buttonPlacement = type === 'panel' ? 'left' : 'right',
|
|
58
|
+
buttonColor = 'primary',
|
|
59
|
+
}: Props,
|
|
60
|
+
) => {
|
|
61
|
+
const t = useDictionary()
|
|
62
|
+
const [enabled, setEnabled] = useState(!validation)
|
|
63
|
+
|
|
64
|
+
function renderValidation() {
|
|
65
|
+
if (!validation) return null
|
|
66
|
+
const value = typeof validation === 'string' ? validation : validation.value
|
|
67
|
+
const label = typeof validation === 'object' && validation.label
|
|
68
|
+
? validation.label
|
|
69
|
+
: interpolate(t.validationLabel, value)
|
|
70
|
+
const placeholder = typeof validation === 'object' ? validation.placeholder : undefined
|
|
71
|
+
return (
|
|
72
|
+
<div style={{ margin: '16px 0' }}>
|
|
73
|
+
<Text>{label}</Text>
|
|
74
|
+
<Input placeholder={placeholder} onChange={e => setEnabled(e.target.value === value)} style={{ marginTop: '10px' }} />
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<OverlayContent title={title} subtitle={subtitle} onClose={onCancel} type={type}>
|
|
81
|
+
<Flex flexDirection="column" flex={1}>
|
|
82
|
+
{message}
|
|
83
|
+
{renderValidation()}
|
|
84
|
+
</Flex>
|
|
85
|
+
{(cancel || confirm) && <Flex gap justifyContent={justifyButtons[buttonPlacement]} alignItems="center" sx={{ mt: 6 }}>
|
|
86
|
+
{cancel && <Button appearance="outlined" colorScheme="inverse" onClick={onCancel}>{cancel}</Button>}
|
|
87
|
+
{confirm && <Button colorScheme={buttonColor} onClick={onConfirm} disabled={!enabled}>
|
|
88
|
+
{confirm}
|
|
89
|
+
</Button>}
|
|
90
|
+
</Flex>}
|
|
91
|
+
</OverlayContent>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { Flex } from '@citric/core'
|
|
2
|
-
import { ReactNode } from 'react'
|
|
3
|
-
import { useAnchorTag } from '../layout-context'
|
|
4
|
-
import { Logo } from '../svg/Logo'
|
|
5
|
-
import { PortalSwitcher, PortalSwitcherProps } from './PortalSwitcher'
|
|
6
|
-
import { SelectionListProps } from './SelectionList'
|
|
7
|
-
import { UserMenu } from './UserMenu'
|
|
8
|
-
|
|
9
|
-
export interface HeaderProps {
|
|
10
|
-
logo?: ReactNode,
|
|
11
|
-
logoHref?: string,
|
|
12
|
-
userName?: string,
|
|
13
|
-
email?: string,
|
|
14
|
-
portalSwitch?: PortalSwitcherProps['portals'],
|
|
15
|
-
options?: SelectionListProps['items'],
|
|
16
|
-
center?: ReactNode,
|
|
17
|
-
right?: ReactNode,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const Header = ({ logo, logoHref, center, right, userName, email, options, portalSwitch }: HeaderProps) => {
|
|
21
|
-
const Link = useAnchorTag()
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<>
|
|
25
|
-
{portalSwitch ?
|
|
26
|
-
<PortalSwitcher portals={portalSwitch} /> :
|
|
27
|
-
<Link href={logoHref} title="Home">{logo ?? <Logo style={{ width: 130 }} />}</Link>
|
|
28
|
-
}
|
|
29
|
-
<Flex flex={1}>{center}</Flex>
|
|
30
|
-
{right}
|
|
31
|
-
{userName && <UserMenu userName={userName} email={email} options={options} />}
|
|
32
|
-
</>
|
|
33
|
-
)
|
|
34
|
-
}
|
|
1
|
+
import { Flex } from '@citric/core'
|
|
2
|
+
import { ReactNode } from 'react'
|
|
3
|
+
import { useAnchorTag } from '../layout-context'
|
|
4
|
+
import { Logo } from '../svg/Logo'
|
|
5
|
+
import { PortalSwitcher, PortalSwitcherProps } from './PortalSwitcher'
|
|
6
|
+
import { SelectionListProps } from './SelectionList'
|
|
7
|
+
import { UserMenu } from './UserMenu'
|
|
8
|
+
|
|
9
|
+
export interface HeaderProps {
|
|
10
|
+
logo?: ReactNode,
|
|
11
|
+
logoHref?: string,
|
|
12
|
+
userName?: string,
|
|
13
|
+
email?: string,
|
|
14
|
+
portalSwitch?: PortalSwitcherProps['portals'],
|
|
15
|
+
options?: SelectionListProps['items'],
|
|
16
|
+
center?: ReactNode,
|
|
17
|
+
right?: ReactNode,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Header = ({ logo, logoHref, center, right, userName, email, options, portalSwitch }: HeaderProps) => {
|
|
21
|
+
const Link = useAnchorTag()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{portalSwitch ?
|
|
26
|
+
<PortalSwitcher portals={portalSwitch} /> :
|
|
27
|
+
<Link href={logoHref} title="Home">{logo ?? <Logo style={{ width: 130 }} />}</Link>
|
|
28
|
+
}
|
|
29
|
+
<Flex flex={1}>{center}</Flex>
|
|
30
|
+
{right}
|
|
31
|
+
{userName && <UserMenu userName={userName} email={email} options={options} />}
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -1,58 +1,58 @@
|
|
|
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
|
-
import { useDictionary } from '../dictionary'
|
|
8
|
-
|
|
9
|
-
export const CLOSE_OVERLAY_ID = 'close-overlay'
|
|
10
|
-
|
|
11
|
-
export interface OverlayContentProps extends WithStyle {
|
|
12
|
-
title: string,
|
|
13
|
-
subtitle?: string,
|
|
14
|
-
children: ReactNode,
|
|
15
|
-
onClose?: () => void,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Props extends OverlayContentProps {
|
|
19
|
-
onClose: () => void,
|
|
20
|
-
type: 'modal' | 'panel',
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const ContentBox = styled.section`
|
|
24
|
-
display: flex;
|
|
25
|
-
flex-direction: column;
|
|
26
|
-
border-radius: 1rem;
|
|
27
|
-
background-color: ${theme.color.light['400']};
|
|
28
|
-
&.modal {
|
|
29
|
-
padding: 32px;
|
|
30
|
-
}
|
|
31
|
-
&.panel {
|
|
32
|
-
padding: 20px;
|
|
33
|
-
display: flex;
|
|
34
|
-
flex-direction: column;
|
|
35
|
-
flex: 1;
|
|
36
|
-
}
|
|
37
|
-
header {
|
|
38
|
-
display: flex;
|
|
39
|
-
flex-direction: row;
|
|
40
|
-
margin-bottom: 1.25rem;
|
|
41
|
-
}
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
export const OverlayContent = ({ children, title, subtitle, className, style, onClose, type }: Props) => {
|
|
45
|
-
const t = useDictionary()
|
|
46
|
-
return (
|
|
47
|
-
<ContentBox style={style} className={listToClass([className, type])}>
|
|
48
|
-
<header>
|
|
49
|
-
<Flex flexDirection="column" flex={1}>
|
|
50
|
-
<Text as="h2" appearance={type === 'modal' ? 'h3' : 'h4'}>{title}</Text>
|
|
51
|
-
{subtitle && <Text appearance="body2" colorScheme="light.700">{subtitle}</Text>}
|
|
52
|
-
</Flex>
|
|
53
|
-
<IconButton onClick={onClose} title={t.close} aria-label={t.close} id={CLOSE_OVERLAY_ID}><TimesMini /></IconButton>
|
|
54
|
-
</header>
|
|
55
|
-
{children}
|
|
56
|
-
</ContentBox>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
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
|
+
import { useDictionary } from '../dictionary'
|
|
8
|
+
|
|
9
|
+
export const CLOSE_OVERLAY_ID = 'close-overlay'
|
|
10
|
+
|
|
11
|
+
export interface OverlayContentProps extends WithStyle {
|
|
12
|
+
title: string,
|
|
13
|
+
subtitle?: string,
|
|
14
|
+
children: ReactNode,
|
|
15
|
+
onClose?: () => void,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props extends OverlayContentProps {
|
|
19
|
+
onClose: () => void,
|
|
20
|
+
type: 'modal' | 'panel',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ContentBox = styled.section`
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
border-radius: 1rem;
|
|
27
|
+
background-color: ${theme.color.light['400']};
|
|
28
|
+
&.modal {
|
|
29
|
+
padding: 32px;
|
|
30
|
+
}
|
|
31
|
+
&.panel {
|
|
32
|
+
padding: 20px;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
flex: 1;
|
|
36
|
+
}
|
|
37
|
+
header {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
margin-bottom: 1.25rem;
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
export const OverlayContent = ({ children, title, subtitle, className, style, onClose, type }: Props) => {
|
|
45
|
+
const t = useDictionary()
|
|
46
|
+
return (
|
|
47
|
+
<ContentBox style={style} className={listToClass([className, type])}>
|
|
48
|
+
<header>
|
|
49
|
+
<Flex flexDirection="column" flex={1}>
|
|
50
|
+
<Text as="h2" appearance={type === 'modal' ? 'h3' : 'h4'}>{title}</Text>
|
|
51
|
+
{subtitle && <Text appearance="body2" colorScheme="light.700">{subtitle}</Text>}
|
|
52
|
+
</Flex>
|
|
53
|
+
<IconButton onClick={onClose} title={t.close} aria-label={t.close} id={CLOSE_OVERLAY_ID}><TimesMini /></IconButton>
|
|
54
|
+
</header>
|
|
55
|
+
{children}
|
|
56
|
+
</ContentBox>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import { Button, Flex, IconBox, Text } from '@citric/core'
|
|
2
|
-
import { ArrowRight, CheckCircleFill, Select } from '@citric/icons'
|
|
3
|
-
import { theme } from '@stack-spot/portal-theme'
|
|
4
|
-
import { useTranslate } from '@stack-spot/portal-translate'
|
|
5
|
-
import { ReactNode, useState } from 'react'
|
|
6
|
-
import styled from 'styled-components'
|
|
7
|
-
import { SelectionList, announce } from '..'
|
|
8
|
-
import { AI } from '../svg/AI'
|
|
9
|
-
import { EDP } from '../svg/EDP'
|
|
10
|
-
import { HUB } from '../svg/HUB'
|
|
11
|
-
import { Logo } from '../svg/Logo'
|
|
12
|
-
import { PortalAcronym } from './types'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const Logos: Record<PortalAcronym, ReactNode> = {
|
|
16
|
-
'AI': <AI />,
|
|
17
|
-
'EDP': <EDP />,
|
|
18
|
-
'HUB': <HUB />,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface Portal { acronym: PortalAcronym, url: string }
|
|
22
|
-
|
|
23
|
-
export interface PortalSwitcherProps {
|
|
24
|
-
portals?: Portal[],
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const PortalSwitcherBox = styled(Flex)`
|
|
28
|
-
flex-direction: column;
|
|
29
|
-
align-items: start;
|
|
30
|
-
z-index: 10;
|
|
31
|
-
|
|
32
|
-
.current-portal {
|
|
33
|
-
padding: 8px;
|
|
34
|
-
border-radius: 4px;
|
|
35
|
-
cursor: pointer;
|
|
36
|
-
&:hover {
|
|
37
|
-
background-color: ${theme.color.light[500]};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.selection-list {
|
|
42
|
-
max-width: 360px;
|
|
43
|
-
box-shadow: 4px 4px 48px 0px #000000;
|
|
44
|
-
position: absolute;
|
|
45
|
-
top: 50px;
|
|
46
|
-
|
|
47
|
-
.selection-list-content {
|
|
48
|
-
padding: 8px;
|
|
49
|
-
border-width: 1px;
|
|
50
|
-
border-style: solid;
|
|
51
|
-
border-color: ${theme.color.light['500']};
|
|
52
|
-
|
|
53
|
-
&> ul {
|
|
54
|
-
display: flex;
|
|
55
|
-
flex-direction: column;
|
|
56
|
-
gap: 8px;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.action {
|
|
60
|
-
padding: 16px;
|
|
61
|
-
background-color: ${theme.color.light['400']};
|
|
62
|
-
border-width: 1px;
|
|
63
|
-
border-style: solid;
|
|
64
|
-
border-color: ${theme.color.light['500']};
|
|
65
|
-
border-radius: 4px;
|
|
66
|
-
|
|
67
|
-
&:hover, &:hover a {
|
|
68
|
-
background-color: ${theme.color.light['500']};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
a {
|
|
72
|
-
height: auto;
|
|
73
|
-
transition: unset;
|
|
74
|
-
align-items: flex-start;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
`
|
|
81
|
-
const PORTAL_SWITCHER_ID = 'PortalSwitcher'
|
|
82
|
-
|
|
83
|
-
export const PortalSwitcher = ({ portals = [] }: PortalSwitcherProps) => {
|
|
84
|
-
const [visible, setVisible] = useState<boolean>(false)
|
|
85
|
-
const t = useTranslate(translations)
|
|
86
|
-
const currentPortal = portals?.find(portal => location.href.startsWith(portal.url))
|
|
87
|
-
|
|
88
|
-
return <PortalSwitcherBox>
|
|
89
|
-
{currentPortal ?
|
|
90
|
-
<Button
|
|
91
|
-
className="current-portal"
|
|
92
|
-
appearance="text"
|
|
93
|
-
colorScheme="light"
|
|
94
|
-
aria-controls={PORTAL_SWITCHER_ID}
|
|
95
|
-
aria-expanded={visible}
|
|
96
|
-
aria-label={`${t.portalSwitcher}: ${currentPortal?.acronym} ${t.selected}`}
|
|
97
|
-
onClick={() => {
|
|
98
|
-
setVisible(true)
|
|
99
|
-
announce(`${t.portalSwitcher} ${t.selected}`)
|
|
100
|
-
}}>
|
|
101
|
-
<Flex alignItems="center">
|
|
102
|
-
{Logos[currentPortal.acronym]}
|
|
103
|
-
<IconBox size="xs" ml={3}>
|
|
104
|
-
<Select />
|
|
105
|
-
</IconBox>
|
|
106
|
-
</Flex>
|
|
107
|
-
</Button> :
|
|
108
|
-
<Logo />}
|
|
109
|
-
<SelectionList
|
|
110
|
-
id={PORTAL_SWITCHER_ID}
|
|
111
|
-
items={portals?.map(portal => ({
|
|
112
|
-
label: {
|
|
113
|
-
id: portal.acronym,
|
|
114
|
-
element: <Flex flexDirection="column">
|
|
115
|
-
{Logos[portal.acronym]}
|
|
116
|
-
<Text appearance="microtext1" mt={3} colorScheme="light.700">{t[portal.acronym]}</Text>
|
|
117
|
-
</Flex>,
|
|
118
|
-
},
|
|
119
|
-
target: '_self',
|
|
120
|
-
href: portal.url,
|
|
121
|
-
active: currentPortal?.acronym == portal.acronym,
|
|
122
|
-
iconActive: <CheckCircleFill aria-label={t.selected} />,
|
|
123
|
-
iconRight: portal.acronym !== currentPortal?.acronym ? <ArrowRight /> : undefined,
|
|
124
|
-
}))}
|
|
125
|
-
visible={visible}
|
|
126
|
-
maxHeight="21rem"
|
|
127
|
-
onHide={() => setVisible(false)} />
|
|
128
|
-
</PortalSwitcherBox >
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const translations = {
|
|
133
|
-
en: {
|
|
134
|
-
EDP: 'Efficient and secure solutions from code to production deployment.',
|
|
135
|
-
AI: 'Speed up coding with efficient suggestions and high-quality results.',
|
|
136
|
-
HUB: 'Discover AI Stacks, knowledge sources, and quick commands, all in one streamlined hub.',
|
|
137
|
-
portalSwitcher: 'Portal switcher',
|
|
138
|
-
selected: 'selected',
|
|
139
|
-
},
|
|
140
|
-
pt: {
|
|
141
|
-
EDP: 'Soluções eficientes e seguras do código até a implantação em produção.',
|
|
142
|
-
AI: 'Acelere o desenvolvimento com sugestões eficientes e resultados de alta qualidade.',
|
|
143
|
-
HUB: 'Descubra AI Stacks, knownledge sources e quick commands, tudo em um hub simplificado.',
|
|
144
|
-
portalSwitcher: 'Seletor de portais',
|
|
145
|
-
selected: 'selecionado',
|
|
146
|
-
},
|
|
147
|
-
}
|
|
1
|
+
import { Button, Flex, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ArrowRight, CheckCircleFill, Select } from '@citric/icons'
|
|
3
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
4
|
+
import { useTranslate } from '@stack-spot/portal-translate'
|
|
5
|
+
import { ReactNode, useState } from 'react'
|
|
6
|
+
import styled from 'styled-components'
|
|
7
|
+
import { SelectionList, announce } from '..'
|
|
8
|
+
import { AI } from '../svg/AI'
|
|
9
|
+
import { EDP } from '../svg/EDP'
|
|
10
|
+
import { HUB } from '../svg/HUB'
|
|
11
|
+
import { Logo } from '../svg/Logo'
|
|
12
|
+
import { PortalAcronym } from './types'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const Logos: Record<PortalAcronym, ReactNode> = {
|
|
16
|
+
'AI': <AI />,
|
|
17
|
+
'EDP': <EDP />,
|
|
18
|
+
'HUB': <HUB />,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Portal { acronym: PortalAcronym, url: string }
|
|
22
|
+
|
|
23
|
+
export interface PortalSwitcherProps {
|
|
24
|
+
portals?: Portal[],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PortalSwitcherBox = styled(Flex)`
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: start;
|
|
30
|
+
z-index: 10;
|
|
31
|
+
|
|
32
|
+
.current-portal {
|
|
33
|
+
padding: 8px;
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
&:hover {
|
|
37
|
+
background-color: ${theme.color.light[500]};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.selection-list {
|
|
42
|
+
max-width: 360px;
|
|
43
|
+
box-shadow: 4px 4px 48px 0px #000000;
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 50px;
|
|
46
|
+
|
|
47
|
+
.selection-list-content {
|
|
48
|
+
padding: 8px;
|
|
49
|
+
border-width: 1px;
|
|
50
|
+
border-style: solid;
|
|
51
|
+
border-color: ${theme.color.light['500']};
|
|
52
|
+
|
|
53
|
+
&> ul {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.action {
|
|
60
|
+
padding: 16px;
|
|
61
|
+
background-color: ${theme.color.light['400']};
|
|
62
|
+
border-width: 1px;
|
|
63
|
+
border-style: solid;
|
|
64
|
+
border-color: ${theme.color.light['500']};
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
|
|
67
|
+
&:hover, &:hover a {
|
|
68
|
+
background-color: ${theme.color.light['500']};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
a {
|
|
72
|
+
height: auto;
|
|
73
|
+
transition: unset;
|
|
74
|
+
align-items: flex-start;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
`
|
|
81
|
+
const PORTAL_SWITCHER_ID = 'PortalSwitcher'
|
|
82
|
+
|
|
83
|
+
export const PortalSwitcher = ({ portals = [] }: PortalSwitcherProps) => {
|
|
84
|
+
const [visible, setVisible] = useState<boolean>(false)
|
|
85
|
+
const t = useTranslate(translations)
|
|
86
|
+
const currentPortal = portals?.find(portal => location.href.startsWith(portal.url))
|
|
87
|
+
|
|
88
|
+
return <PortalSwitcherBox>
|
|
89
|
+
{currentPortal ?
|
|
90
|
+
<Button
|
|
91
|
+
className="current-portal"
|
|
92
|
+
appearance="text"
|
|
93
|
+
colorScheme="light"
|
|
94
|
+
aria-controls={PORTAL_SWITCHER_ID}
|
|
95
|
+
aria-expanded={visible}
|
|
96
|
+
aria-label={`${t.portalSwitcher}: ${currentPortal?.acronym} ${t.selected}`}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setVisible(true)
|
|
99
|
+
announce(`${t.portalSwitcher} ${t.selected}`)
|
|
100
|
+
}}>
|
|
101
|
+
<Flex alignItems="center">
|
|
102
|
+
{Logos[currentPortal.acronym]}
|
|
103
|
+
<IconBox size="xs" ml={3}>
|
|
104
|
+
<Select />
|
|
105
|
+
</IconBox>
|
|
106
|
+
</Flex>
|
|
107
|
+
</Button> :
|
|
108
|
+
<Logo />}
|
|
109
|
+
<SelectionList
|
|
110
|
+
id={PORTAL_SWITCHER_ID}
|
|
111
|
+
items={portals?.map(portal => ({
|
|
112
|
+
label: {
|
|
113
|
+
id: portal.acronym,
|
|
114
|
+
element: <Flex flexDirection="column">
|
|
115
|
+
{Logos[portal.acronym]}
|
|
116
|
+
<Text appearance="microtext1" mt={3} colorScheme="light.700">{t[portal.acronym]}</Text>
|
|
117
|
+
</Flex>,
|
|
118
|
+
},
|
|
119
|
+
target: '_self',
|
|
120
|
+
href: portal.url,
|
|
121
|
+
active: currentPortal?.acronym == portal.acronym,
|
|
122
|
+
iconActive: <CheckCircleFill aria-label={t.selected} />,
|
|
123
|
+
iconRight: portal.acronym !== currentPortal?.acronym ? <ArrowRight /> : undefined,
|
|
124
|
+
}))}
|
|
125
|
+
visible={visible}
|
|
126
|
+
maxHeight="21rem"
|
|
127
|
+
onHide={() => setVisible(false)} />
|
|
128
|
+
</PortalSwitcherBox >
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
const translations = {
|
|
133
|
+
en: {
|
|
134
|
+
EDP: 'Efficient and secure solutions from code to production deployment.',
|
|
135
|
+
AI: 'Speed up coding with efficient suggestions and high-quality results.',
|
|
136
|
+
HUB: 'Discover AI Stacks, knowledge sources, and quick commands, all in one streamlined hub.',
|
|
137
|
+
portalSwitcher: 'Portal switcher',
|
|
138
|
+
selected: 'selected',
|
|
139
|
+
},
|
|
140
|
+
pt: {
|
|
141
|
+
EDP: 'Soluções eficientes e seguras do código até a implantação em produção.',
|
|
142
|
+
AI: 'Acelere o desenvolvimento com sugestões eficientes e resultados de alta qualidade.',
|
|
143
|
+
HUB: 'Descubra AI Stacks, knownledge sources e quick commands, tudo em um hub simplificado.',
|
|
144
|
+
portalSwitcher: 'Seletor de portais',
|
|
145
|
+
selected: 'selecionado',
|
|
146
|
+
},
|
|
147
|
+
}
|