@stack-spot/portal-layout 0.0.49 → 0.0.51

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 (112) hide show
  1. package/dist/Layout.d.ts +5 -4
  2. package/dist/Layout.d.ts.map +1 -1
  3. package/dist/Layout.js +4 -3
  4. package/dist/Layout.js.map +1 -1
  5. package/dist/LayoutOverlayManager.js +6 -6
  6. package/dist/components/Dialog.d.ts +1 -1
  7. package/dist/components/Dialog.js +1 -1
  8. package/dist/components/Header.d.ts +1 -1
  9. package/dist/components/Header.d.ts.map +1 -1
  10. package/dist/components/Header.js +8 -4
  11. package/dist/components/Header.js.map +1 -1
  12. package/dist/components/OverlayContent.d.ts +1 -1
  13. package/dist/components/OverlayContent.js +20 -20
  14. package/dist/components/PortalSwitcher.d.ts +1 -1
  15. package/dist/components/PortalSwitcher.js +54 -54
  16. package/dist/components/SelectionList.d.ts +1 -1
  17. package/dist/components/SelectionList.d.ts.map +1 -1
  18. package/dist/components/SelectionList.js +61 -58
  19. package/dist/components/SelectionList.js.map +1 -1
  20. package/dist/components/Toaster.d.ts +1 -1
  21. package/dist/components/Toaster.js +1 -1
  22. package/dist/components/UserMenu.d.ts +1 -1
  23. package/dist/components/UserMenu.js +42 -42
  24. package/dist/components/UserMenu.js.map +1 -1
  25. package/dist/components/error/ErrorBoundary.d.ts +1 -1
  26. package/dist/components/error/ErrorBoundary.js +1 -1
  27. package/dist/components/error/ErrorDescriptor.d.ts +12 -0
  28. package/dist/components/error/ErrorDescriptor.d.ts.map +1 -0
  29. package/dist/components/error/ErrorDescriptor.js +17 -0
  30. package/dist/components/error/ErrorDescriptor.js.map +1 -0
  31. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  32. package/dist/components/error/ErrorFeedback.js +1 -1
  33. package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
  34. package/dist/components/error/SilentErrorBoundary.js +1 -1
  35. package/dist/components/menu/MenuContent.d.ts +8 -12
  36. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  37. package/dist/components/menu/MenuContent.js +151 -148
  38. package/dist/components/menu/MenuContent.js.map +1 -1
  39. package/dist/components/menu/MenuSections.d.ts +1 -1
  40. package/dist/components/menu/MenuSections.d.ts.map +1 -1
  41. package/dist/components/menu/MenuSections.js +7 -5
  42. package/dist/components/menu/MenuSections.js.map +1 -1
  43. package/dist/components/menu/PageSelector.d.ts +1 -1
  44. package/dist/components/menu/PageSelector.d.ts.map +1 -1
  45. package/dist/components/menu/PageSelector.js +68 -66
  46. package/dist/components/menu/PageSelector.js.map +1 -1
  47. package/dist/components/menu/useCheckTextOverflow.d.ts +6 -0
  48. package/dist/components/menu/useCheckTextOverflow.d.ts.map +1 -0
  49. package/dist/components/menu/useCheckTextOverflow.js +20 -0
  50. package/dist/components/menu/useCheckTextOverflow.js.map +1 -0
  51. package/dist/layout-context.d.ts +10 -0
  52. package/dist/layout-context.d.ts.map +1 -0
  53. package/dist/layout-context.js +11 -0
  54. package/dist/layout-context.js.map +1 -0
  55. package/dist/layout.css +465 -465
  56. package/dist/svg/AI.d.ts +1 -1
  57. package/dist/svg/AI.js +1 -1
  58. package/dist/svg/EDP.d.ts +1 -1
  59. package/dist/svg/EDP.js +1 -1
  60. package/dist/svg/Forbidden.d.ts +1 -1
  61. package/dist/svg/Forbidden.js +1 -1
  62. package/dist/svg/HUB.d.ts +1 -1
  63. package/dist/svg/HUB.js +1 -1
  64. package/dist/svg/Logo.d.ts +1 -1
  65. package/dist/svg/Logo.js +1 -1
  66. package/dist/svg/NotFound.d.ts +1 -1
  67. package/dist/svg/NotFound.js +1 -1
  68. package/dist/svg/ServerError.d.ts +1 -1
  69. package/dist/svg/ServerError.js +1 -1
  70. package/dist/svg/Unauthenticated.d.ts +1 -1
  71. package/dist/svg/Unauthenticated.js +1 -1
  72. package/dist/toaster.js +2 -2
  73. package/dist/toaster.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/Layout.tsx +106 -103
  76. package/src/LayoutOverlayManager.tsx +273 -273
  77. package/src/components/Dialog.tsx +93 -93
  78. package/src/components/Header.tsx +34 -29
  79. package/src/components/OverlayContent.tsx +58 -58
  80. package/src/components/PortalSwitcher.tsx +147 -147
  81. package/src/components/SelectionList.tsx +272 -268
  82. package/src/components/Toaster.tsx +16 -16
  83. package/src/components/UserMenu.tsx +111 -111
  84. package/src/components/error/ErrorBoundary.tsx +38 -38
  85. package/src/components/error/ErrorFeedback.tsx +114 -114
  86. package/src/components/error/ErrorManager.ts +31 -31
  87. package/src/components/error/SilentErrorBoundary.tsx +54 -54
  88. package/src/components/menu/MenuContent.tsx +296 -293
  89. package/src/components/menu/MenuSections.tsx +270 -268
  90. package/src/components/menu/PageSelector.tsx +154 -152
  91. package/src/components/menu/constants.ts +2 -2
  92. package/src/components/menu/types.ts +112 -112
  93. package/src/components/menu/use-check-text-overflow.tsx +26 -26
  94. package/src/components/menu/use-keyboard-controls.tsx +70 -70
  95. package/src/components/types.ts +15 -15
  96. package/src/dictionary.ts +25 -25
  97. package/src/elements.ts +24 -24
  98. package/src/errors.ts +11 -11
  99. package/src/index.ts +17 -17
  100. package/src/layout-context.tsx +22 -0
  101. package/src/layout.css +465 -465
  102. package/src/svg/AI.tsx +37 -37
  103. package/src/svg/EDP.tsx +35 -35
  104. package/src/svg/Forbidden.tsx +22 -22
  105. package/src/svg/HUB.tsx +35 -35
  106. package/src/svg/Logo.tsx +35 -35
  107. package/src/svg/NotFound.tsx +16 -16
  108. package/src/svg/ServerError.tsx +33 -33
  109. package/src/svg/Unauthenticated.tsx +16 -16
  110. package/src/toaster.tsx +76 -76
  111. package/src/utils.ts +114 -114
  112. 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,29 +1,34 @@
1
- import { Flex } from '@citric/core'
2
- import { ReactNode } from 'react'
3
- import { Logo } from '../svg/Logo'
4
- import { PortalSwitcher, PortalSwitcherProps } from './PortalSwitcher'
5
- import { SelectionListProps } from './SelectionList'
6
- import { UserMenu } from './UserMenu'
7
-
8
- export interface HeaderProps {
9
- logo?: ReactNode,
10
- logoHref?: string,
11
- userName?: string,
12
- email?: string,
13
- portalSwitch?: PortalSwitcherProps['portals'],
14
- options?: SelectionListProps['items'],
15
- center?: ReactNode,
16
- right?: ReactNode,
17
- }
18
-
19
- export const Header = ({ logo, logoHref, center, right, userName, email, options, portalSwitch }: HeaderProps) => (
20
- <>
21
- {portalSwitch ?
22
- <PortalSwitcher portals={portalSwitch} /> :
23
- <a href={logoHref} title="Home">{logo ?? <Logo style={{ width: 130 }} />}</a>
24
- }
25
- <Flex flex={1}>{center}</Flex>
26
- {right}
27
- {userName && <UserMenu userName={userName} email={email} options={options} />}
28
- </>
29
- )
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
+ }