@blocklet/ui-react 2.1.9 → 2.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Footer/brand.js +67 -0
- package/lib/Footer/copyright.js +44 -0
- package/lib/Footer/index.js +67 -30
- package/lib/Footer/links.js +129 -0
- package/lib/Footer/row.js +59 -0
- package/lib/Footer/social-media.js +75 -0
- package/lib/Header/index.js +12 -2
- package/lib/Icon/index.js +81 -0
- package/lib/utils.js +34 -0
- package/package.json +5 -4
- package/src/Footer/brand.js +70 -0
- package/src/Footer/copyright.js +26 -0
- package/src/Footer/index.js +51 -71
- package/src/Footer/links.js +202 -0
- package/src/Footer/row.js +53 -0
- package/src/Footer/social-media.js +52 -0
- package/src/Header/index.js +7 -1
- package/src/Icon/index.js +45 -0
- package/src/utils.js +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/ui-react",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.13",
|
|
4
4
|
"description": "Some useful front-end web components that can be used in Blocklets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"url": "https://github.com/ArcBlock/ux/issues"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@arcblock/did-connect": "^2.1.
|
|
38
|
-
"@arcblock/ux": "^2.1.
|
|
37
|
+
"@arcblock/did-connect": "^2.1.13",
|
|
38
|
+
"@arcblock/ux": "^2.1.13",
|
|
39
39
|
"@iconify/iconify": "^2.2.1",
|
|
40
40
|
"@mui/material": "^5.6.4",
|
|
41
41
|
"core-js": "^3.6.4",
|
|
42
|
+
"react-error-boundary": "^3.1.4",
|
|
42
43
|
"styled-components": "^5.0.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
@@ -56,5 +57,5 @@
|
|
|
56
57
|
"eslint-plugin-react-hooks": "^4.2.0",
|
|
57
58
|
"jest": "^24.1.0"
|
|
58
59
|
},
|
|
59
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "ab9ffbc57b99ede75ed0ddd8f090e3d8e8fb97d1"
|
|
60
61
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import useTheme from '@mui/styles/useTheme';
|
|
5
|
+
|
|
6
|
+
export default function Brand({ name, logo, description, ...rest }) {
|
|
7
|
+
const theme = useTheme();
|
|
8
|
+
if (!name && !logo && !description) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const logoElement = React.isValidElement(logo) ? logo : <img src={logo} alt={name} />;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Root {...rest} $theme={theme}>
|
|
16
|
+
<div>
|
|
17
|
+
{logo && <div className="footer-brand-logo">{logoElement}</div>}
|
|
18
|
+
{name && <div className="footer-brand-name">{name}</div>}
|
|
19
|
+
</div>
|
|
20
|
+
{description && <div className="footer-brand-desc">{description}</div>}
|
|
21
|
+
</Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Brand.propTypes = {
|
|
26
|
+
name: PropTypes.node,
|
|
27
|
+
logo: PropTypes.node,
|
|
28
|
+
description: PropTypes.string,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
Brand.defaultProps = {
|
|
32
|
+
name: '',
|
|
33
|
+
logo: '',
|
|
34
|
+
description: '',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const Root = styled.div`
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
width: 240px;
|
|
41
|
+
font-size: 14px;
|
|
42
|
+
a {
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
color: inherit;
|
|
45
|
+
}
|
|
46
|
+
> div:first-child {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
}
|
|
50
|
+
.footer-brand-logo {
|
|
51
|
+
height: 32px;
|
|
52
|
+
margin-right: 8px;
|
|
53
|
+
img,
|
|
54
|
+
svg {
|
|
55
|
+
max-width: 100%;
|
|
56
|
+
max-height: 100%;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
.footer-brand-name {
|
|
60
|
+
font-size: 16px;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
}
|
|
63
|
+
.footer-brand-desc {
|
|
64
|
+
margin-top: 16px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
${props => props.$theme.breakpoints.down('sm')} {
|
|
68
|
+
width: auto;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
export default function Copyright({ owner, year, ...rest }) {
|
|
6
|
+
return (
|
|
7
|
+
<Root {...rest}>
|
|
8
|
+
Copyright © {year} {owner}
|
|
9
|
+
</Root>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Copyright.propTypes = {
|
|
14
|
+
owner: PropTypes.string,
|
|
15
|
+
year: PropTypes.string,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
Copyright.defaultProps = {
|
|
19
|
+
owner: 'ArcBlock, Inc.',
|
|
20
|
+
year: `${new Date().getFullYear()}`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const Root = styled.p`
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
`;
|
package/src/Footer/index.js
CHANGED
|
@@ -1,48 +1,68 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import useTheme from '@mui/styles/useTheme';
|
|
4
|
+
import Box from '@mui/material/Box';
|
|
4
5
|
import Container from '@mui/material/Container';
|
|
5
|
-
import
|
|
6
|
+
import { withErrorBoundary } from 'react-error-boundary';
|
|
7
|
+
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
8
|
+
import Brand from './brand';
|
|
9
|
+
import Links from './links';
|
|
10
|
+
import SocialMedia from './social-media';
|
|
11
|
+
import Copyright from './copyright';
|
|
12
|
+
import Row from './row';
|
|
13
|
+
import { mapRecursive } from '../utils';
|
|
6
14
|
|
|
7
15
|
import { blockletMetaProps } from '../types';
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* 专门用于 (composable) blocklet 的 Footer 组件, 基于 blocklet meta 中的数据渲染
|
|
11
19
|
*/
|
|
12
|
-
|
|
20
|
+
function Footer({ meta, ...rest }) {
|
|
13
21
|
const theme = useTheme();
|
|
14
22
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
15
23
|
if (!blocklet.appName) {
|
|
16
24
|
return null;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const {
|
|
28
|
+
appLogo,
|
|
29
|
+
appName,
|
|
30
|
+
theme: blockletTheme,
|
|
31
|
+
navigation = [],
|
|
32
|
+
copyrightOwner,
|
|
33
|
+
footerNavigation,
|
|
34
|
+
footerLinks,
|
|
35
|
+
socialMedia,
|
|
36
|
+
} = blocklet;
|
|
37
|
+
const navMenuItems = mapRecursive(
|
|
38
|
+
// TODO: 需要讨论 blocklet.yml 是否需要专门为 footer navigation 提供配置, 暂时与 header 共用 navigation 配置
|
|
39
|
+
footerNavigation || navigation,
|
|
40
|
+
item => ({
|
|
41
|
+
...item,
|
|
42
|
+
label: item.title,
|
|
43
|
+
link: item.link,
|
|
44
|
+
}),
|
|
45
|
+
'items'
|
|
46
|
+
);
|
|
27
47
|
|
|
28
48
|
return (
|
|
29
49
|
<Root {...rest} $bgcolor={blockletTheme?.background} $theme={theme}>
|
|
30
50
|
<Container>
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
</
|
|
51
|
+
<Row>
|
|
52
|
+
<Box>
|
|
53
|
+
<Brand
|
|
54
|
+
name={appName}
|
|
55
|
+
logo={appLogo}
|
|
56
|
+
description="Official DID Wallet webapp implementation that makes it possible to manage your digital identities and assets from the browser."
|
|
57
|
+
/>
|
|
58
|
+
<SocialMedia items={socialMedia} style={{ marginTop: 24 }} />
|
|
59
|
+
</Box>
|
|
60
|
+
<Links links={navMenuItems} />
|
|
61
|
+
</Row>
|
|
62
|
+
<Row autoCenter style={{ marginTop: 72 }}>
|
|
63
|
+
{<Copyright owner={copyrightOwner || appName} />}
|
|
64
|
+
{<Links flowLayout links={footerLinks} style={{ color: '#999' }} />}
|
|
65
|
+
</Row>
|
|
46
66
|
</Container>
|
|
47
67
|
</Root>
|
|
48
68
|
);
|
|
@@ -57,52 +77,12 @@ Footer.defaultProps = {
|
|
|
57
77
|
};
|
|
58
78
|
|
|
59
79
|
const Root = styled.div`
|
|
60
|
-
padding:
|
|
80
|
+
padding: 48px 0;
|
|
61
81
|
border-top: 1px solid #eee;
|
|
62
|
-
color:
|
|
82
|
+
color: ${props => props.$theme.palette.grey[600]};
|
|
63
83
|
${({ $bgcolor }) => $bgcolor && `background-color: ${$bgcolor};`}
|
|
64
|
-
.footer_line {
|
|
65
|
-
display: flex;
|
|
66
|
-
justify-content: space-between;
|
|
67
|
-
}
|
|
68
|
-
.footer_line + .footer_line {
|
|
69
|
-
margin-top: 32px;
|
|
70
|
-
}
|
|
71
|
-
.footer_brand {
|
|
72
|
-
display: flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
color: #777;
|
|
75
|
-
img {
|
|
76
|
-
height: 36px;
|
|
77
|
-
}
|
|
78
|
-
span {
|
|
79
|
-
margin-left: 8px;
|
|
80
|
-
font-size: 20px;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
.footer_nav {
|
|
84
|
-
.navmenu {
|
|
85
|
-
padding-left: 0;
|
|
86
|
-
padding-right: 0;
|
|
87
|
-
}
|
|
88
|
-
> * {
|
|
89
|
-
background: transparent;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
${props => props.$theme.breakpoints.down('md')} {
|
|
93
|
-
.footer_line {
|
|
94
|
-
flex-direction: column;
|
|
95
|
-
}
|
|
96
|
-
.footer_line + .footer_line {
|
|
97
|
-
margin-top: 0;
|
|
98
|
-
}
|
|
99
|
-
.footer_brand {
|
|
100
|
-
img {
|
|
101
|
-
height: 24px;
|
|
102
|
-
}
|
|
103
|
-
span {
|
|
104
|
-
font-size: 16px;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
84
|
`;
|
|
85
|
+
|
|
86
|
+
export default withErrorBoundary(Footer, {
|
|
87
|
+
FallbackComponent: ErrorFallback,
|
|
88
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/* eslint-disable react/no-array-index-key */
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
import useTheme from '@mui/styles/useTheme';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* footer 中的 links (支持分组, 最多支持 2 级)
|
|
10
|
+
* TODO: dark/light theme
|
|
11
|
+
*/
|
|
12
|
+
export default function Links({ links, flowLayout, ...rest }) {
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
15
|
+
if (!links?.length) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
// 只要发现一项元素有子元素, 就认为是分组 (大字号突出 group title)
|
|
19
|
+
const isGroupMode = links.some(item => item.items?.length);
|
|
20
|
+
const renderItem = ({ label, link, render, props }) => {
|
|
21
|
+
let result = label;
|
|
22
|
+
if (render) {
|
|
23
|
+
result = render({ label, link, props });
|
|
24
|
+
} else if (link) {
|
|
25
|
+
result = (
|
|
26
|
+
<a href={link} {...props}>
|
|
27
|
+
{label}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Root
|
|
36
|
+
{...rest}
|
|
37
|
+
className={clsx(rest.className, {
|
|
38
|
+
'footer-links--grouped': isGroupMode,
|
|
39
|
+
'footer-links--flow': flowLayout,
|
|
40
|
+
})}
|
|
41
|
+
$theme={theme}>
|
|
42
|
+
<div className="footer-links-inner">
|
|
43
|
+
{flowLayout &&
|
|
44
|
+
links.map((item, i) => (
|
|
45
|
+
<span key={i} className="footer-links-item">
|
|
46
|
+
{renderItem(item)}
|
|
47
|
+
</span>
|
|
48
|
+
))}
|
|
49
|
+
{!flowLayout &&
|
|
50
|
+
links.map((item, i) => {
|
|
51
|
+
const { items } = item;
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={i}
|
|
55
|
+
className={clsx('footer-links-group', {
|
|
56
|
+
'footer-links-group--active': i === activeIndex,
|
|
57
|
+
})}
|
|
58
|
+
onClick={() => setActiveIndex(activeIndex === i ? -1 : i)}>
|
|
59
|
+
<span className="footer-links-item">{renderItem(item)}</span>
|
|
60
|
+
{!!items?.length && (
|
|
61
|
+
<div className="footer-links-sub">
|
|
62
|
+
{items.map((child, j) => (
|
|
63
|
+
<span key={j} className="footer-links-item">
|
|
64
|
+
{renderItem(child)}
|
|
65
|
+
</span>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</Root>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Links.propTypes = {
|
|
78
|
+
links: PropTypes.arrayOf(
|
|
79
|
+
PropTypes.shape({
|
|
80
|
+
label: PropTypes.string,
|
|
81
|
+
link: PropTypes.string,
|
|
82
|
+
render: PropTypes.func,
|
|
83
|
+
props: PropTypes.object,
|
|
84
|
+
})
|
|
85
|
+
),
|
|
86
|
+
// 流动布局, 简单的从左到右排列
|
|
87
|
+
flowLayout: PropTypes.bool,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
Links.defaultProps = {
|
|
91
|
+
links: [],
|
|
92
|
+
flowLayout: false,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const Root = styled.div`
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
color: ${props => props.$theme.palette.grey[600]};
|
|
98
|
+
.footer-links-inner {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
margin: 0 -8px;
|
|
102
|
+
}
|
|
103
|
+
.footer-links-group,
|
|
104
|
+
.footer-links-sub {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
}
|
|
108
|
+
.footer-links-item {
|
|
109
|
+
display: inline-block;
|
|
110
|
+
padding: 0 8px;
|
|
111
|
+
font-size: 14px;
|
|
112
|
+
line-height: 1.6;
|
|
113
|
+
}
|
|
114
|
+
.footer-links-group + .footer-links-group {
|
|
115
|
+
margin-left: 128px;
|
|
116
|
+
}
|
|
117
|
+
&.footer-links--grouped {
|
|
118
|
+
.footer-links-group {
|
|
119
|
+
> .footer-links-item {
|
|
120
|
+
font-weight: bold;
|
|
121
|
+
}
|
|
122
|
+
.footer-links-sub {
|
|
123
|
+
margin-top: 8px;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
a {
|
|
128
|
+
color: inherit;
|
|
129
|
+
text-decoration: none;
|
|
130
|
+
&:hover {
|
|
131
|
+
text-decoration: underline;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&.footer-links--flow {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
.footer-links-inner {
|
|
138
|
+
justify-content: center;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
margin: 0 -8px;
|
|
141
|
+
.footer-links-item {
|
|
142
|
+
padding: 0 8px;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
${props => props.$theme.breakpoints.down('lg')} {
|
|
148
|
+
.footer-links-group + .footer-links-group {
|
|
149
|
+
margin-left: 56px;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
${props => props.$theme.breakpoints.down('lg')} {
|
|
154
|
+
.footer-links-group + .footer-links-group {
|
|
155
|
+
margin-left: 40px;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
${props => props.$theme.breakpoints.down('sm')} {
|
|
160
|
+
.footer-links-inner {
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
margin: 0;
|
|
163
|
+
}
|
|
164
|
+
.footer-links-sub {
|
|
165
|
+
display: none;
|
|
166
|
+
}
|
|
167
|
+
.footer-links-group {
|
|
168
|
+
padding: 12px 0;
|
|
169
|
+
border-top: 1px solid ${props => props.$theme.palette.grey[200]};
|
|
170
|
+
}
|
|
171
|
+
.footer-links-group + .footer-links-group {
|
|
172
|
+
margin-left: 0;
|
|
173
|
+
}
|
|
174
|
+
.footer-links-group--active {
|
|
175
|
+
.footer-links-sub {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: row;
|
|
178
|
+
flex-wrap: wrap;
|
|
179
|
+
.footer-links-item {
|
|
180
|
+
flex: 0 0 50%;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
.footer-links-item {
|
|
185
|
+
padding: 0;
|
|
186
|
+
font-size: 13px;
|
|
187
|
+
}
|
|
188
|
+
&.footer-links--grouped {
|
|
189
|
+
.footer-links-group {
|
|
190
|
+
> .footer-links-item {
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
&.footer-links--flow {
|
|
197
|
+
.footer-links-inner {
|
|
198
|
+
flex-direction: row;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import useTheme from '@mui/styles/useTheme';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
|
|
7
|
+
export default function Row({ children, autoCenter, ...rest }) {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
if (!children) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<RowRoot
|
|
14
|
+
{...rest}
|
|
15
|
+
className={clsx(rest.className, { 'footer-row-auto-center': autoCenter })}
|
|
16
|
+
$theme={theme}>
|
|
17
|
+
{children}
|
|
18
|
+
</RowRoot>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Row.propTypes = {
|
|
23
|
+
children: PropTypes.any,
|
|
24
|
+
autoCenter: PropTypes.bool,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
Row.defaultProps = {
|
|
28
|
+
children: null,
|
|
29
|
+
autoCenter: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RowRoot = styled.div`
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
& + & {
|
|
36
|
+
margin-top: 24px;
|
|
37
|
+
}
|
|
38
|
+
&.footer-row-auto-center > *:only-child {
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
${props => props.$theme.breakpoints.down('md')} {
|
|
43
|
+
align-items: stretch;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: 16px;
|
|
46
|
+
> * {
|
|
47
|
+
flex: 1 0 100%;
|
|
48
|
+
}
|
|
49
|
+
&.footer-row-auto-center > * {
|
|
50
|
+
margin: 0 auto;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import useTheme from '@mui/styles/useTheme';
|
|
5
|
+
import Icon from '../Icon';
|
|
6
|
+
|
|
7
|
+
export default function SocialMedia({ items, ...rest }) {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
if (!items?.length) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<Root {...rest}>
|
|
14
|
+
{items.map((item, i) => {
|
|
15
|
+
return (
|
|
16
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
17
|
+
<a key={i} href={item.link} target="_blank" rel="noreferrer">
|
|
18
|
+
<Icon
|
|
19
|
+
icon={item.icon}
|
|
20
|
+
sx={{ bgcolor: theme.palette.grey[600], color: '#fff' }}
|
|
21
|
+
size={44}
|
|
22
|
+
variant="rounded"
|
|
23
|
+
component="span"
|
|
24
|
+
/>
|
|
25
|
+
</a>
|
|
26
|
+
);
|
|
27
|
+
})}
|
|
28
|
+
</Root>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SocialMedia.propTypes = {
|
|
33
|
+
items: PropTypes.arrayOf(
|
|
34
|
+
PropTypes.shape({
|
|
35
|
+
// icon 对应 Icon#icon prop, 支持 iconify name 和 url 2 种形式:
|
|
36
|
+
icon: PropTypes.string,
|
|
37
|
+
link: PropTypes.string,
|
|
38
|
+
})
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
SocialMedia.defaultProps = {
|
|
43
|
+
items: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const Root = styled.div`
|
|
47
|
+
display: inline-flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
a + a {
|
|
50
|
+
margin-left: 12px;
|
|
51
|
+
}
|
|
52
|
+
`;
|
package/src/Header/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
import { ThemeProvider } from '@mui/material/styles';
|
|
5
|
+
import { withErrorBoundary } from 'react-error-boundary';
|
|
6
|
+
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
5
7
|
import { create } from '@arcblock/ux/lib/Theme';
|
|
6
8
|
import { ResponsiveHeader } from '@arcblock/ux/lib/Header';
|
|
7
9
|
import NavMenu from '@arcblock/ux/lib/NavMenu';
|
|
@@ -58,7 +60,7 @@ const parseNavigation = navigation => {
|
|
|
58
60
|
* 专门用于 (composable) blocklet 的 Header 组件, 解析 blocklet meta 中的数据, 通过组合 UX 中的 Header & NavMenu 组件来渲染
|
|
59
61
|
*/
|
|
60
62
|
// eslint-disable-next-line no-shadow
|
|
61
|
-
|
|
63
|
+
function Header({ meta, addons, sessionManagerProps, ...rest }) {
|
|
62
64
|
const sessionInfo = React.useContext(SessionContext);
|
|
63
65
|
const { locale } = useLocaleContext() || {};
|
|
64
66
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
@@ -157,3 +159,7 @@ const StyledUxHeader = styled(ResponsiveHeader)`
|
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
161
|
`;
|
|
162
|
+
|
|
163
|
+
export default withErrorBoundary(Header, {
|
|
164
|
+
FallbackComponent: ErrorFallback,
|
|
165
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Avatar from '@mui/material/Avatar';
|
|
4
|
+
import '@iconify/iconify';
|
|
5
|
+
import { isUrl } from '../utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Icon 组件, 基于 mui Avatar 组件扩展对 iconify 的支持
|
|
9
|
+
*/
|
|
10
|
+
export default function Icon({ icon, size, ...rest }) {
|
|
11
|
+
const sx = { ...rest.sx };
|
|
12
|
+
if (size) {
|
|
13
|
+
sx.width = size;
|
|
14
|
+
sx.height = size;
|
|
15
|
+
}
|
|
16
|
+
if (isUrl(icon)) {
|
|
17
|
+
return <Avatar {...rest} src={icon} sx={sx} />;
|
|
18
|
+
}
|
|
19
|
+
if (icon) {
|
|
20
|
+
// y = 0.6 * x + 4
|
|
21
|
+
const iconHeight = size ? 0.6 * size + 4 : 0;
|
|
22
|
+
return (
|
|
23
|
+
<Avatar {...rest} sx={sx}>
|
|
24
|
+
<span
|
|
25
|
+
className="iconify"
|
|
26
|
+
data-icon={icon}
|
|
27
|
+
{...(iconHeight && { 'data-height': iconHeight })}
|
|
28
|
+
/>
|
|
29
|
+
</Avatar>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Icon.propTypes = {
|
|
36
|
+
// icon 支持 2 种形式:
|
|
37
|
+
// 1. iconify icon name: <prefix>:<name>
|
|
38
|
+
// 2. url
|
|
39
|
+
icon: PropTypes.string.isRequired,
|
|
40
|
+
size: PropTypes.number,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Icon.defaultProps = {
|
|
44
|
+
size: null,
|
|
45
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const mapRecursive = (array, fn, childrenKey = 'children') => {
|
|
2
|
+
return array.map(item => {
|
|
3
|
+
if (Array.isArray(item[childrenKey])) {
|
|
4
|
+
return fn({
|
|
5
|
+
...item,
|
|
6
|
+
[childrenKey]: mapRecursive(item[childrenKey], fn, childrenKey),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return fn(item);
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// "http://", "https://", "//" 开头的 3 种情况
|
|
14
|
+
export const isUrl = str => {
|
|
15
|
+
return /^(https?:)?\/\//.test(str);
|
|
16
|
+
};
|