@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.1.9",
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.9",
38
- "@arcblock/ux": "^2.1.9",
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": "7a3efb456e1dbe3318ab8757feb9e58a88c08d8e"
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
+ `;
@@ -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 NavMenu from '@arcblock/ux/lib/NavMenu';
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
- export default function Footer({ meta, ...rest }) {
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 { appLogo, appName, theme: blockletTheme, navigation = [] } = blocklet;
20
- // TODO: 支持分组的 links (#590)
21
- // 暂时只支持扁平的 links, 没有 link 属性的 navigation item 会被忽略
22
- const navMenuItems = navigation
23
- .filter(item => item.link)
24
- .map(item => ({
25
- label: <a href={item.link}>{item.title}</a>,
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
- <div className="footer_line">
32
- <div className="footer_brand">
33
- <img src={appLogo} alt="logo" />
34
- <span>{appName}</span>
35
- </div>
36
- <div className="footer_nav">
37
- {!!navMenuItems?.length && <NavMenu items={navMenuItems} />}
38
- </div>
39
- </div>
40
- <div className="footer_line">
41
- <div className="footer_copyright">
42
- <span>Powered by Blocklet Server</span>
43
- </div>
44
- <div className="footer_extra" />
45
- </div>
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: 32px 40px;
80
+ padding: 48px 0;
61
81
  border-top: 1px solid #eee;
62
- color: #9397a1;
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
+ `;
@@ -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
- export default function Header({ meta, addons, sessionManagerProps, ...rest }) {
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
+ };