@stack-spot/portal-layout 0.0.51 → 0.0.52

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