@blocklet/ui-react 2.6.8 → 2.7.0

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.
@@ -0,0 +1,81 @@
1
+ import PropTypes from 'prop-types';
2
+ import Box from '@mui/material/Box';
3
+ import Container from '@mui/material/Container';
4
+ import { styled } from '@arcblock/ux/lib/Theme';
5
+ import Row from './row';
6
+
7
+ /**
8
+ * footer standard layout
9
+ */
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { jsxs as _jsxs } from "react/jsx-runtime";
12
+ function StandardLayout({
13
+ elements,
14
+ data,
15
+ ...rest
16
+ }) {
17
+ return /*#__PURE__*/_jsx(Root, {
18
+ ...rest,
19
+ children: /*#__PURE__*/_jsxs(Container, {
20
+ children: [/*#__PURE__*/_jsxs(Box, {
21
+ sx: {
22
+ display: 'flex',
23
+ flexDirection: {
24
+ xs: 'column',
25
+ md: 'row'
26
+ },
27
+ justifyContent: 'space-between',
28
+ alignItems: {
29
+ xs: 'center',
30
+ md: 'space-between'
31
+ },
32
+ gap: 2,
33
+ pb: 3
34
+ },
35
+ children: [/*#__PURE__*/_jsx(Box, {
36
+ children: elements.brand
37
+ }), /*#__PURE__*/_jsx(Box, {
38
+ lineHeight: 1,
39
+ children: elements.socialMedia
40
+ })]
41
+ }), !!data.navigation?.length && /*#__PURE__*/_jsx(Box, {
42
+ sx: {
43
+ mb: 6,
44
+ pt: 3,
45
+ borderTop: 1,
46
+ borderColor: 'grey.200'
47
+ },
48
+ children: elements.navigation
49
+ }), /*#__PURE__*/_jsxs(Row, {
50
+ sx: {
51
+ pt: 3,
52
+ borderTop: 1,
53
+ borderColor: 'grey.200'
54
+ },
55
+ autoCenter: true,
56
+ children: [elements.copyright, elements.links]
57
+ })]
58
+ })
59
+ });
60
+ }
61
+ StandardLayout.propTypes = {
62
+ elements: PropTypes.shape({
63
+ brand: PropTypes.element,
64
+ navigation: PropTypes.element,
65
+ socialMedia: PropTypes.element,
66
+ copyright: PropTypes.element,
67
+ links: PropTypes.element
68
+ }).isRequired,
69
+ data: PropTypes.object.isRequired
70
+ };
71
+ const Root = styled('div')`
72
+ padding: 32px 0 24px 0;
73
+ .footer-brand-name,
74
+ .footer-brand-desc {
75
+ display: none;
76
+ }
77
+ && .footer-brand-logo {
78
+ margin-right: 0;
79
+ }
80
+ `;
81
+ export default StandardLayout;
@@ -0,0 +1,238 @@
1
+ /* eslint-disable react/no-array-index-key */
2
+ import { useState } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { styled } from '@arcblock/ux/lib/Theme';
5
+ import clsx from 'clsx';
6
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
7
+ import Icon from '../Icon';
8
+
9
+ /**
10
+ * footer 中的 links (支持分组, 最多支持 2 级)
11
+ * TODO: dark/light theme
12
+ */
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ import { Fragment as _Fragment } from "react/jsx-runtime";
15
+ import { jsxs as _jsxs } from "react/jsx-runtime";
16
+ export default function Links({
17
+ links,
18
+ flowLayout,
19
+ ...rest
20
+ }) {
21
+ const [activeIndex, setActiveIndex] = useState(-1);
22
+ if (!links?.length) {
23
+ return null;
24
+ }
25
+ // 只要发现一项元素有子元素, 就认为是分组 (大字号突出 group title)
26
+ const isGroupMode = links.some(item => item.items?.length);
27
+ const renderItem = ({
28
+ label,
29
+ link,
30
+ icon,
31
+ render,
32
+ props
33
+ }) => {
34
+ let result = label;
35
+ if (render) {
36
+ result = render({
37
+ label,
38
+ link,
39
+ props
40
+ });
41
+ } else if (link) {
42
+ result = /*#__PURE__*/_jsx("a", {
43
+ href: link,
44
+ ...props,
45
+ children: label
46
+ });
47
+ }
48
+ return /*#__PURE__*/_jsxs(_Fragment, {
49
+ children: [icon && /*#__PURE__*/_jsx(Icon, {
50
+ icon: icon,
51
+ size: 20,
52
+ sx: {
53
+ mr: 0.5
54
+ }
55
+ }), result]
56
+ });
57
+ };
58
+ return /*#__PURE__*/_jsx(Root, {
59
+ ...rest,
60
+ className: clsx(rest.className, {
61
+ 'footer-links--grouped': isGroupMode,
62
+ 'footer-links--flow': flowLayout
63
+ }),
64
+ children: /*#__PURE__*/_jsxs("div", {
65
+ className: "footer-links-inner",
66
+ children: [flowLayout && links.map((item, i) => /*#__PURE__*/_jsx("span", {
67
+ className: "footer-links-item",
68
+ children: renderItem(item)
69
+ }, i)), !flowLayout && links.map((item, i) => {
70
+ const {
71
+ items
72
+ } = item;
73
+ const isActive = i === activeIndex;
74
+ return /*#__PURE__*/_jsxs("div", {
75
+ className: clsx('footer-links-group', {
76
+ 'footer-links-group--active': isActive
77
+ }),
78
+ onClick: () => setActiveIndex(activeIndex === i ? -1 : i),
79
+ children: [/*#__PURE__*/_jsxs("span", {
80
+ className: "footer-links-item",
81
+ children: [renderItem(item), !!items?.length && /*#__PURE__*/_jsx("span", {
82
+ className: "footer-links-group-expand-icon",
83
+ children: /*#__PURE__*/_jsx(ExpandMoreIcon, {
84
+ style: {
85
+ transform: `rotate(${isActive ? 180 : 0}deg)`
86
+ }
87
+ })
88
+ })]
89
+ }), !!items?.length && /*#__PURE__*/_jsx("div", {
90
+ className: "footer-links-sub",
91
+ children: items.map((child, j) => /*#__PURE__*/_jsx("span", {
92
+ className: "footer-links-item",
93
+ children: renderItem(child)
94
+ }, j))
95
+ })]
96
+ }, i);
97
+ })]
98
+ })
99
+ });
100
+ }
101
+ Links.propTypes = {
102
+ links: PropTypes.arrayOf(PropTypes.shape({
103
+ label: PropTypes.string,
104
+ link: PropTypes.string,
105
+ render: PropTypes.func,
106
+ props: PropTypes.object
107
+ })),
108
+ // 流动布局, 简单的从左到右排列
109
+ flowLayout: PropTypes.bool
110
+ };
111
+ Links.defaultProps = {
112
+ links: [],
113
+ flowLayout: false
114
+ };
115
+ const Root = styled('div')`
116
+ overflow: hidden;
117
+ color: ${props => props.theme.palette.grey[600]};
118
+ .footer-links-inner {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ margin: 0 -8px;
122
+ }
123
+ .footer-links-group,
124
+ .footer-links-sub {
125
+ display: flex;
126
+ flex-direction: column;
127
+ }
128
+ .footer-links-sub .footer-links-item {
129
+ color: ${props => props.theme.palette.grey[900]};
130
+ }
131
+ .footer-links-group-expand-icon {
132
+ display: none;
133
+ position: absolute;
134
+ right: 16px;
135
+ top: 50%;
136
+ transform: translate(0, -50%);
137
+ line-height: 1;
138
+ svg {
139
+ width: auto;
140
+ height: 0.75em;
141
+ }
142
+ }
143
+ .footer-links-item {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ position: relative;
147
+ padding: 4px 8px;
148
+ font-size: 14px;
149
+ }
150
+ &.footer-links--grouped {
151
+ .footer-links-group {
152
+ > .footer-links-item {
153
+ font-weight: bold;
154
+ }
155
+ .footer-links-sub {
156
+ margin-top: 8px;
157
+ }
158
+ }
159
+ }
160
+ a {
161
+ display: inline-block;
162
+ max-width: 150px;
163
+ color: inherit;
164
+ text-decoration: none;
165
+ &:hover {
166
+ text-decoration: underline;
167
+ }
168
+ }
169
+
170
+ &.footer-links--flow {
171
+ display: inline-flex;
172
+ .footer-links-inner {
173
+ justify-content: center;
174
+ flex-wrap: wrap;
175
+ margin: 0 -8px;
176
+ .footer-links-item {
177
+ padding: 0 8px;
178
+ }
179
+ .footer-links-item + .footer-links-item::before {
180
+ content: '';
181
+ position: absolute;
182
+ left: 0;
183
+ top: 50%;
184
+ transform: translate(0, -50%);
185
+ height: 1em;
186
+ border-left: 1px solid ${props => props.theme.palette.grey[400]};
187
+ }
188
+ }
189
+ }
190
+
191
+ ${props => props.theme.breakpoints.down('md')} {
192
+ .footer-links-inner {
193
+ flex-direction: column;
194
+ margin: 0;
195
+ }
196
+ .footer-links-sub {
197
+ display: none;
198
+ }
199
+ .footer-links-group {
200
+ position: relative;
201
+ padding: 12px 0;
202
+ .footer-links-item .footer-links-group-expand-icon {
203
+ display: inline-block;
204
+ }
205
+ }
206
+ .footer-links-group + .footer-links-group {
207
+ border-top: 1px solid ${props => props.theme.palette.grey[200]};
208
+ }
209
+ .footer-links-group--active {
210
+ .footer-links-sub {
211
+ display: flex;
212
+ flex-direction: row;
213
+ flex-wrap: wrap;
214
+ .footer-links-item {
215
+ flex: 0 0 50%;
216
+ }
217
+ }
218
+ }
219
+ .footer-links-item {
220
+ padding-left: 0;
221
+ padding-right: 0;
222
+ font-size: 13px;
223
+ }
224
+ &.footer-links--grouped {
225
+ .footer-links-group {
226
+ > .footer-links-item {
227
+ font-size: 14px;
228
+ }
229
+ }
230
+ }
231
+
232
+ &.footer-links--flow {
233
+ .footer-links-inner {
234
+ flex-direction: row;
235
+ }
236
+ }
237
+ }
238
+ `;
@@ -0,0 +1,63 @@
1
+ import PropTypes from 'prop-types';
2
+ import { useTheme, styled } from '@arcblock/ux/lib/Theme';
3
+ import Icon from '../Icon';
4
+ import { jsx as _jsx } from "react/jsx-runtime";
5
+ export default function SocialMedia({
6
+ items,
7
+ ...rest
8
+ }) {
9
+ const theme = useTheme();
10
+ if (!items?.length) {
11
+ return null;
12
+ }
13
+ return /*#__PURE__*/_jsx(Root, {
14
+ ...rest,
15
+ children: items.map((item, i) => {
16
+ return (
17
+ /*#__PURE__*/
18
+ // eslint-disable-next-line react/no-array-index-key
19
+ _jsx("a", {
20
+ href: item.link,
21
+ target: "_blank",
22
+ rel: "noreferrer",
23
+ children: /*#__PURE__*/_jsx(Icon, {
24
+ icon: item.icon || item.title,
25
+ sx: {
26
+ bgcolor: theme.palette.grey[600],
27
+ color: '#fff'
28
+ },
29
+ size: 24,
30
+ component: "span"
31
+ })
32
+ }, i)
33
+ );
34
+ })
35
+ });
36
+ }
37
+ SocialMedia.propTypes = {
38
+ items: PropTypes.arrayOf(PropTypes.shape({
39
+ // icon 对应 Icon#icon prop, 支持 iconify name 和 url 2 种形式:
40
+ icon: PropTypes.string,
41
+ link: PropTypes.string
42
+ }))
43
+ };
44
+ SocialMedia.defaultProps = {
45
+ items: null
46
+ };
47
+ const Root = styled('div')`
48
+ display: inline-flex;
49
+ flex-wrap: wrap;
50
+ align-items: center;
51
+ justify-content: center;
52
+ gap: 20px;
53
+ a {
54
+ color: ${props => props.theme.palette.grey[400]};
55
+ text-decoration: none;
56
+ &:hover {
57
+ color: ${props => props.theme.palette.primary.light};
58
+ }
59
+ }
60
+ ${props => props.theme.breakpoints.down('md')} {
61
+ gap: 12px;
62
+ }
63
+ `;
@@ -0,0 +1,194 @@
1
+ import { useMemo } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { withErrorBoundary } from 'react-error-boundary';
4
+ import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
5
+ import { styled } from '@arcblock/ux/lib/Theme';
6
+ import { ResponsiveHeader } from '@arcblock/ux/lib/Header';
7
+ import NavMenu from '@arcblock/ux/lib/NavMenu';
8
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
9
+ import Icon from '../Icon';
10
+ import OverridableThemeProvider from '../common/overridable-theme-provider';
11
+ import { blockletMetaProps, sessionManagerProps } from '../types';
12
+ import { mapRecursive, flatRecursive, matchPaths } from '../utils';
13
+ import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
14
+ import HeaderAddons from '../common/header-addons';
15
+ import { useWalletHiddenTopbar } from '../common/wallet-hidden-topbar';
16
+
17
+ // blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
18
+ import { jsx as _jsx } from "react/jsx-runtime";
19
+ const parseNavigation = navigation => {
20
+ if (!navigation?.length) {
21
+ return {
22
+ navItems: [],
23
+ activeId: null
24
+ };
25
+ }
26
+ let counter = 1;
27
+ const parseItem = item => {
28
+ const icon = item.icon ? /*#__PURE__*/_jsx(Icon, {
29
+ icon: item.icon
30
+ }) : null;
31
+ if (item.items) {
32
+ return {
33
+ id: `${counter++}`,
34
+ label: item.title,
35
+ icon,
36
+ children: item.items
37
+ };
38
+ }
39
+ let props = {};
40
+ if (item.link?.startsWith('http://') || item.link?.startsWith('https://')) {
41
+ props = {
42
+ target: '_blank',
43
+ rel: 'noreferrer'
44
+ };
45
+ }
46
+ return {
47
+ id: `${counter++}`,
48
+ label: /*#__PURE__*/_jsx("a", {
49
+ href: item.link,
50
+ ...props,
51
+ children: item.title
52
+ }),
53
+ icon,
54
+ link: item.link
55
+ };
56
+ };
57
+ const navItems = mapRecursive(navigation, parseItem, 'items');
58
+ const flattened = flatRecursive(navItems);
59
+ const matchedIndex = matchPaths(flattened.map(item => item.link));
60
+ return {
61
+ navItems,
62
+ activeId: matchedIndex >= 0 ? flattened[matchedIndex].id : null
63
+ };
64
+ };
65
+
66
+ /**
67
+ * 专门用于 (composable) blocklet 的 Header 组件, 解析 blocklet meta 中的数据, 通过组合 UX 中的 Header & NavMenu 组件来渲染
68
+ */
69
+ // eslint-disable-next-line no-shadow
70
+ function Header({
71
+ meta,
72
+ addons,
73
+ sessionManagerProps,
74
+ homeLink,
75
+ theme: themeOverrides,
76
+ ...rest
77
+ }) {
78
+ useWalletHiddenTopbar();
79
+ const {
80
+ locale
81
+ } = useLocaleContext() || {};
82
+ const formattedBlocklet = useMemo(() => {
83
+ const blocklet = Object.assign({}, window.blocklet, meta);
84
+ try {
85
+ return formatBlockletInfo(blocklet);
86
+ } catch (e) {
87
+ console.error('Failed to format blocklet info', e, blocklet);
88
+ return blocklet;
89
+ }
90
+ }, [meta]);
91
+ if (!formattedBlocklet.appName) {
92
+ return null;
93
+ }
94
+ const {
95
+ appLogo,
96
+ appLogoRect,
97
+ theme
98
+ } = formattedBlocklet;
99
+ const navigation = getLocalizedNavigation(formattedBlocklet?.navigation?.header, locale);
100
+ const parsedNavigation = parseNavigation(navigation);
101
+ const {
102
+ navItems,
103
+ activeId
104
+ } = parsedNavigation;
105
+ const _addons = typeof addons === 'function' ? builtInAddons => addons(builtInAddons, {
106
+ navigation: parsedNavigation
107
+ }) : addons;
108
+ const headerAddons = /*#__PURE__*/_jsx(HeaderAddons, {
109
+ formattedBlocklet: formattedBlocklet,
110
+ addons: _addons,
111
+ sessionManagerProps: sessionManagerProps
112
+ });
113
+ return /*#__PURE__*/_jsx(OverridableThemeProvider, {
114
+ theme: themeOverrides,
115
+ children: /*#__PURE__*/_jsx(StyledUxHeader, {
116
+ homeLink: homeLink,
117
+ logo: /*#__PURE__*/_jsx("img", {
118
+ src: appLogoRect || appLogo,
119
+ alt: "logo"
120
+ }),
121
+ addons: headerAddons,
122
+ ...rest,
123
+ $bgcolor: theme?.background?.header,
124
+ children: !navItems?.length ? null : ({
125
+ isMobile
126
+ }) => /*#__PURE__*/_jsx(NavMenu, {
127
+ mode: isMobile ? 'inline' : 'horizontal',
128
+ activeId: activeId,
129
+ items: navItems,
130
+ className: "header-nav",
131
+ bgColor: "transparent",
132
+ textColor: "#888"
133
+ })
134
+ })
135
+ });
136
+ }
137
+ Header.propTypes = {
138
+ meta: blockletMetaProps,
139
+ // 需要考虑 定制的 addons 与内置的 连接钱包/选择语言 addons 共存的情况
140
+ // - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
141
+ // - PropTypes.node: 将 addons 原样传给 UX Header 组件
142
+ addons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
143
+ sessionManagerProps,
144
+ homeLink: PropTypes.string,
145
+ // 允许覆盖 header 内置的 theme
146
+ theme: PropTypes.object
147
+ };
148
+ Header.defaultProps = {
149
+ meta: {},
150
+ addons: null,
151
+ sessionManagerProps: {
152
+ showRole: true
153
+ },
154
+ homeLink: publicPath,
155
+ theme: null
156
+ };
157
+ const StyledUxHeader = styled(ResponsiveHeader)`
158
+ ${({
159
+ $bgcolor
160
+ }) => `background-color: ${$bgcolor || '#fff'};`}
161
+ font-family: Lato, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
162
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
163
+ .header-logo {
164
+ min-width: 44px;
165
+ }
166
+ ${props => props.theme.breakpoints.down('md')} {
167
+ .header-logo {
168
+ min-width: 32px;
169
+ }
170
+ }
171
+ .header-nav {
172
+ .navmenu-sub .navmenu-item {
173
+ min-width: 80px;
174
+ }
175
+ }
176
+ .header-nav.navmenu--horizontal {
177
+ .navmenu-root > .navmenu-sub,
178
+ .navmenu-root > .navmenu-item {
179
+ padding: 16px 4px;
180
+
181
+ .navmenu-sub-container {
182
+ padding-top: 0;
183
+ }
184
+ }
185
+ .navmenu-item-icon > .MuiAvatar-root,
186
+ .navmenu-sub-icon > .MuiAvatar-root {
187
+ width: 20px;
188
+ height: 20px;
189
+ }
190
+ }
191
+ `;
192
+ export default withErrorBoundary(Header, {
193
+ FallbackComponent: ErrorFallback
194
+ });
@@ -0,0 +1,90 @@
1
+ import PropTypes from 'prop-types';
2
+ import Avatar from '@mui/material/Avatar';
3
+ import 'iconify-icon';
4
+ import { isUrl, isIconifyString } from '../utils';
5
+
6
+ /**
7
+ * Icon 组件, 基于 mui Avatar 组件扩展对 iconify 的支持
8
+ */
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ export default function Icon({
11
+ icon,
12
+ size,
13
+ sx,
14
+ ...rest
15
+ }) {
16
+ const _sx = [...(Array.isArray(sx) ? sx : [sx])];
17
+ if (size) {
18
+ _sx.push({
19
+ width: size,
20
+ height: size
21
+ });
22
+ }
23
+ // 禁用默认的 circular variant 样式
24
+ if (!rest.variant) {
25
+ _sx.push({
26
+ '&.MuiAvatar-root': {
27
+ color: 'inherit',
28
+ fontWeight: 'bold',
29
+ backgroundColor: 'transparent',
30
+ borderRadius: 0
31
+ },
32
+ // 无 icon 背景时, svg icon 尺寸与窗口尺寸一致
33
+ '&.MuiAvatar-root svg': {
34
+ width: '100%',
35
+ height: '100%'
36
+ }
37
+ });
38
+ }
39
+ if (isUrl(icon)) {
40
+ return /*#__PURE__*/_jsx(Avatar, {
41
+ as: "span",
42
+ ...rest,
43
+ src: icon,
44
+ sx: _sx
45
+ });
46
+ }
47
+ if (isIconifyString(icon)) {
48
+ // y = 0.6 * x + 4
49
+ const height = size ? 0.6 * size + 4 : 0;
50
+ return /*#__PURE__*/_jsx(Avatar, {
51
+ as: "span",
52
+ ...rest,
53
+ sx: _sx,
54
+ children: /*#__PURE__*/_jsx("iconify-icon", {
55
+ icon: icon,
56
+ height: height || undefined
57
+ })
58
+ });
59
+ }
60
+ // letter avatar
61
+ if (icon && typeof icon === 'string') {
62
+ _sx.push({
63
+ '&.MuiAvatar-root': {
64
+ display: 'inline-flex',
65
+ ...(size && {
66
+ fontSize: size - 2
67
+ })
68
+ }
69
+ });
70
+ return /*#__PURE__*/_jsx(Avatar, {
71
+ as: "span",
72
+ ...rest,
73
+ sx: _sx,
74
+ children: Array.from(icon)[0]
75
+ });
76
+ }
77
+ return null;
78
+ }
79
+ Icon.propTypes = {
80
+ // icon 支持 2 种形式:
81
+ // 1. iconify icon name: <prefix>:<name>
82
+ // 2. url
83
+ icon: PropTypes.string.isRequired,
84
+ size: PropTypes.number,
85
+ sx: PropTypes.oneOfType([PropTypes.array, PropTypes.func, PropTypes.object])
86
+ };
87
+ Icon.defaultProps = {
88
+ size: null,
89
+ sx: null
90
+ };