@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.
Files changed (97) hide show
  1. package/dist/Layout.d.ts +2 -2
  2. package/dist/Layout.js +1 -1
  3. package/dist/LayoutOverlayManager.js +6 -6
  4. package/dist/LayoutOverlayManager.js.map +1 -1
  5. package/dist/components/Dialog.d.ts +1 -1
  6. package/dist/components/Dialog.js +1 -1
  7. package/dist/components/Header.d.ts +1 -1
  8. package/dist/components/Header.js +1 -1
  9. package/dist/components/OverlayContent.d.ts +1 -1
  10. package/dist/components/OverlayContent.js +20 -20
  11. package/dist/components/PortalSwitcher.d.ts +1 -1
  12. package/dist/components/PortalSwitcher.js +54 -54
  13. package/dist/components/SelectionList.d.ts +1 -1
  14. package/dist/components/SelectionList.js +54 -54
  15. package/dist/components/SelectionList.js.map +1 -1
  16. package/dist/components/Toaster.d.ts +1 -1
  17. package/dist/components/Toaster.js +1 -1
  18. package/dist/components/UserMenu.d.ts +1 -1
  19. package/dist/components/UserMenu.js +41 -41
  20. package/dist/components/error/ErrorBoundary.d.ts +1 -1
  21. package/dist/components/error/ErrorBoundary.js +1 -1
  22. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  23. package/dist/components/error/ErrorFeedback.js +1 -1
  24. package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
  25. package/dist/components/error/SilentErrorBoundary.js +1 -1
  26. package/dist/components/menu/MenuContent.d.ts +12 -10
  27. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  28. package/dist/components/menu/MenuContent.js +146 -146
  29. package/dist/components/menu/MenuContent.js.map +1 -1
  30. package/dist/components/menu/MenuSections.d.ts +1 -1
  31. package/dist/components/menu/MenuSections.js +1 -1
  32. package/dist/components/menu/MenuSections.js.map +1 -1
  33. package/dist/components/menu/PageSelector.d.ts +1 -1
  34. package/dist/components/menu/PageSelector.js +65 -65
  35. package/dist/components/menu/PageSelector.js.map +1 -1
  36. package/dist/components/menu/use-check-text-overflow.js.map +1 -1
  37. package/dist/layout-context.d.ts +1 -1
  38. package/dist/layout-context.js +1 -1
  39. package/dist/layout.css +466 -466
  40. package/dist/svg/AI.d.ts +1 -1
  41. package/dist/svg/AI.js +1 -1
  42. package/dist/svg/EDP.d.ts +1 -1
  43. package/dist/svg/EDP.js +1 -1
  44. package/dist/svg/Forbidden.d.ts +1 -1
  45. package/dist/svg/Forbidden.js +1 -1
  46. package/dist/svg/HUB.d.ts +1 -1
  47. package/dist/svg/HUB.js +1 -1
  48. package/dist/svg/Logo.d.ts +1 -1
  49. package/dist/svg/Logo.js +1 -1
  50. package/dist/svg/NotFound.d.ts +1 -1
  51. package/dist/svg/NotFound.js +1 -1
  52. package/dist/svg/ServerError.d.ts +1 -1
  53. package/dist/svg/ServerError.js +1 -1
  54. package/dist/svg/Unauthenticated.d.ts +1 -1
  55. package/dist/svg/Unauthenticated.js +1 -1
  56. package/dist/toaster.js +2 -2
  57. package/dist/toaster.js.map +1 -1
  58. package/dist/utils.js.map +1 -1
  59. package/package.json +5 -5
  60. package/src/Layout.tsx +106 -106
  61. package/src/LayoutOverlayManager.tsx +273 -273
  62. package/src/components/Dialog.tsx +93 -93
  63. package/src/components/Header.tsx +34 -34
  64. package/src/components/OverlayContent.tsx +58 -58
  65. package/src/components/PortalSwitcher.tsx +147 -147
  66. package/src/components/SelectionList.tsx +272 -272
  67. package/src/components/Toaster.tsx +16 -16
  68. package/src/components/UserMenu.tsx +111 -111
  69. package/src/components/error/ErrorBoundary.tsx +38 -38
  70. package/src/components/error/ErrorFeedback.tsx +114 -114
  71. package/src/components/error/ErrorManager.ts +31 -31
  72. package/src/components/error/SilentErrorBoundary.tsx +54 -54
  73. package/src/components/menu/MenuContent.tsx +296 -296
  74. package/src/components/menu/MenuSections.tsx +270 -270
  75. package/src/components/menu/PageSelector.tsx +154 -154
  76. package/src/components/menu/constants.ts +2 -2
  77. package/src/components/menu/types.ts +112 -112
  78. package/src/components/menu/use-check-text-overflow.tsx +26 -26
  79. package/src/components/menu/use-keyboard-controls.tsx +70 -70
  80. package/src/components/types.ts +15 -15
  81. package/src/dictionary.ts +25 -25
  82. package/src/elements.ts +24 -24
  83. package/src/errors.ts +11 -11
  84. package/src/index.ts +17 -17
  85. package/src/layout-context.tsx +22 -22
  86. package/src/layout.css +466 -466
  87. package/src/svg/AI.tsx +37 -37
  88. package/src/svg/EDP.tsx +35 -35
  89. package/src/svg/Forbidden.tsx +22 -22
  90. package/src/svg/HUB.tsx +35 -35
  91. package/src/svg/Logo.tsx +35 -35
  92. package/src/svg/NotFound.tsx +16 -16
  93. package/src/svg/ServerError.tsx +33 -33
  94. package/src/svg/Unauthenticated.tsx +16 -16
  95. package/src/toaster.tsx +76 -76
  96. package/src/utils.ts +114 -114
  97. 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
+ }