@blocklet/ui-react 2.6.9 → 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.
- package/babel.config.es.js +8 -0
- package/es/Dashboard/index.js +151 -0
- package/es/Footer/brand.js +90 -0
- package/es/Footer/copyright.js +27 -0
- package/es/Footer/index.js +98 -0
- package/es/Footer/internal-footer.js +144 -0
- package/es/Footer/layout/plain.js +57 -0
- package/es/Footer/layout/row.js +51 -0
- package/es/Footer/layout/standard.js +81 -0
- package/es/Footer/links.js +238 -0
- package/es/Footer/social-media.js +63 -0
- package/es/Header/index.js +194 -0
- package/es/Icon/index.js +90 -0
- package/es/blocklets.js +167 -0
- package/es/common/header-addons.js +93 -0
- package/es/common/link-blocker.js +25 -0
- package/es/common/overridable-theme-provider.js +25 -0
- package/es/common/wallet-hidden-topbar.js +14 -0
- package/es/types.js +37 -0
- package/es/utils.js +83 -0
- package/lib/Dashboard/index.js +2 -3
- package/lib/Icon/index.js +6 -8
- package/lib/common/header-addons.js +3 -4
- package/package.json +37 -6
- package/src/Dashboard/index.js +1 -1
- package/src/Icon/index.js +3 -3
- package/src/common/header-addons.js +1 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
presets: [
|
|
3
|
+
['@babel/preset-env', { modules: false, targets: 'chrome 114' }],
|
|
4
|
+
['@babel/preset-react', { useBuiltIns: true, runtime: 'automatic' }],
|
|
5
|
+
],
|
|
6
|
+
plugins: ['babel-plugin-inline-react-svg'],
|
|
7
|
+
ignore: ['src/**/*.stories.js', 'src/**/demo'],
|
|
8
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* eslint-disable no-shadow */
|
|
2
|
+
import { useMemo, useLayoutEffect, useContext } from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
5
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import UxDashboard from '@arcblock/ux/lib/Layout/dashboard';
|
|
7
|
+
import { blockletMetaProps, sessionManagerProps } from '../types';
|
|
8
|
+
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
|
|
9
|
+
import { publicPath, formatBlockletInfo, getLocalizedNavigation, filterNavByRole } from '../blocklets';
|
|
10
|
+
import HeaderAddons from '../common/header-addons';
|
|
11
|
+
import { useWalletHiddenTopbar } from '../common/wallet-hidden-topbar';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 专门用于 (composable) blocklet 的 Dashboard 组件, 解析 blocklet meta 中 section 为 dashboard 的 navigation 数据, 渲染一个 UX Dashboard
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line no-shadow
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
function Dashboard({
|
|
19
|
+
meta,
|
|
20
|
+
fallbackUrl,
|
|
21
|
+
invalidPathFallback,
|
|
22
|
+
headerAddons,
|
|
23
|
+
sessionManagerProps,
|
|
24
|
+
links,
|
|
25
|
+
...rest
|
|
26
|
+
}) {
|
|
27
|
+
useWalletHiddenTopbar();
|
|
28
|
+
const sessionCtx = useContext(SessionContext);
|
|
29
|
+
const user = sessionCtx?.session?.user;
|
|
30
|
+
const userRole = user?.role;
|
|
31
|
+
const {
|
|
32
|
+
locale
|
|
33
|
+
} = useLocaleContext() || {};
|
|
34
|
+
const formattedBlocklet = useMemo(() => {
|
|
35
|
+
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
36
|
+
try {
|
|
37
|
+
return formatBlockletInfo(blocklet);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('Failed to format blocklet info', e, blocklet);
|
|
40
|
+
return blocklet;
|
|
41
|
+
}
|
|
42
|
+
}, [meta]);
|
|
43
|
+
const {
|
|
44
|
+
localizedNav,
|
|
45
|
+
flattened,
|
|
46
|
+
matchedIndex
|
|
47
|
+
} = useMemo(() => {
|
|
48
|
+
let localizedNav = getLocalizedNavigation(formattedBlocklet?.navigation?.dashboard, locale) || [];
|
|
49
|
+
// 根据 role 筛选 nav 数据
|
|
50
|
+
localizedNav = filterNavByRole(localizedNav, userRole);
|
|
51
|
+
// 将 nav 数据处理成 ux dashboard 需要的格式
|
|
52
|
+
localizedNav = mapRecursive(localizedNav, item => ({
|
|
53
|
+
title: item.title,
|
|
54
|
+
url: item.link,
|
|
55
|
+
icon: item.icon ? /*#__PURE__*/_jsx("iconify-icon", {
|
|
56
|
+
icon: item.icon
|
|
57
|
+
}) : null,
|
|
58
|
+
// https://github.com/ArcBlock/ux/issues/755#issuecomment-1208692620
|
|
59
|
+
external: true,
|
|
60
|
+
children: item.items
|
|
61
|
+
}), 'items');
|
|
62
|
+
// 展平后使用 matchPaths 检测 link#active 状态
|
|
63
|
+
const flattened = flatRecursive(localizedNav).filter(item => !!item.url);
|
|
64
|
+
const matchedIndex = matchPaths(flattened.map(item => item.url));
|
|
65
|
+
if (matchedIndex !== -1) {
|
|
66
|
+
flattened[matchedIndex].active = true;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
localizedNav,
|
|
70
|
+
flattened,
|
|
71
|
+
matchedIndex
|
|
72
|
+
};
|
|
73
|
+
}, [formattedBlocklet, locale, userRole]);
|
|
74
|
+
const allLinks = typeof links === 'function' ? links(localizedNav) : [...localizedNav, ...links];
|
|
75
|
+
|
|
76
|
+
// 页面初始化时, 如果当前用户没有权限访问任何导航菜单 (比如登录时未提供 VC 导致无权限), 则跳转到 fallbackUrl
|
|
77
|
+
// 未认证 (user 为空) 时不做处理, 这种情况的页面跳转逻辑一般由应用自行处理
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
if (!!user && !flattened?.length && fallbackUrl) {
|
|
80
|
+
window.location.href = fallbackUrl;
|
|
81
|
+
}
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
}, [fallbackUrl]);
|
|
84
|
+
|
|
85
|
+
// 导航菜单变动且存在可用菜单但无匹配项时 (如切换 passport), 跳转到首个菜单项
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
if (!!user && !!flattened?.length && matchedIndex === -1) {
|
|
88
|
+
if (invalidPathFallback) {
|
|
89
|
+
invalidPathFallback();
|
|
90
|
+
} else {
|
|
91
|
+
window.location.href = flattened[0]?.url || publicPath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
+
}, [invalidPathFallback, flattened, matchedIndex]);
|
|
96
|
+
if (!formattedBlocklet.appName) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const {
|
|
100
|
+
appLogo,
|
|
101
|
+
appLogoRect,
|
|
102
|
+
appName
|
|
103
|
+
} = formattedBlocklet;
|
|
104
|
+
const _headerAddons = /*#__PURE__*/_jsx(HeaderAddons, {
|
|
105
|
+
formattedBlocklet: formattedBlocklet,
|
|
106
|
+
addons: headerAddons,
|
|
107
|
+
sessionManagerProps: sessionManagerProps
|
|
108
|
+
});
|
|
109
|
+
return /*#__PURE__*/_jsx(UxDashboard, {
|
|
110
|
+
title: appName,
|
|
111
|
+
fullWidth: true,
|
|
112
|
+
sidebarWidth: 128,
|
|
113
|
+
legacy: false,
|
|
114
|
+
links: allLinks,
|
|
115
|
+
...rest,
|
|
116
|
+
headerProps: {
|
|
117
|
+
homeLink: publicPath,
|
|
118
|
+
logo: /*#__PURE__*/_jsx("img", {
|
|
119
|
+
src: appLogoRect || appLogo,
|
|
120
|
+
alt: "logo"
|
|
121
|
+
}),
|
|
122
|
+
addons: _headerAddons,
|
|
123
|
+
...rest.headerProps
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
Dashboard.propTypes = {
|
|
128
|
+
meta: blockletMetaProps,
|
|
129
|
+
// 如果当前用户没有权限访问任何导航菜单, 则自动跳转到 fallbackUrl, 默认值为 publicPath, 设置为 null 表示禁用自动跳转
|
|
130
|
+
fallbackUrl: PropTypes.string,
|
|
131
|
+
// 当前路径未匹配任何 nav links 时的 fallback, 默认行为跳转到首个可用的 nav link
|
|
132
|
+
invalidPathFallback: PropTypes.func,
|
|
133
|
+
headerAddons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
|
134
|
+
sessionManagerProps,
|
|
135
|
+
links: PropTypes.oneOfType([PropTypes.array, PropTypes.func])
|
|
136
|
+
};
|
|
137
|
+
Dashboard.defaultProps = {
|
|
138
|
+
meta: {},
|
|
139
|
+
fallbackUrl: publicPath,
|
|
140
|
+
invalidPathFallback: null,
|
|
141
|
+
headerAddons: undefined,
|
|
142
|
+
sessionManagerProps: {
|
|
143
|
+
showRole: true,
|
|
144
|
+
// dashboard 默认退出登录行为: 跳转到 (root) blocklet 首页
|
|
145
|
+
onLogout: () => {
|
|
146
|
+
window.location.href = publicPath;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
links: []
|
|
150
|
+
};
|
|
151
|
+
export default Dashboard;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
4
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
6
|
+
export default function Brand({
|
|
7
|
+
name,
|
|
8
|
+
logo,
|
|
9
|
+
description,
|
|
10
|
+
...rest
|
|
11
|
+
}) {
|
|
12
|
+
if (!name && !logo && !description) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const logoElement = /*#__PURE__*/isValidElement(logo) ? logo : /*#__PURE__*/_jsx("img", {
|
|
16
|
+
src: logo,
|
|
17
|
+
alt: name
|
|
18
|
+
});
|
|
19
|
+
return /*#__PURE__*/_jsxs(Root, {
|
|
20
|
+
...rest,
|
|
21
|
+
children: [/*#__PURE__*/_jsxs("div", {
|
|
22
|
+
children: [logo && /*#__PURE__*/_jsx("div", {
|
|
23
|
+
className: "footer-brand-logo",
|
|
24
|
+
children: logoElement
|
|
25
|
+
}), name && /*#__PURE__*/_jsx("div", {
|
|
26
|
+
className: "footer-brand-name",
|
|
27
|
+
children: name
|
|
28
|
+
})]
|
|
29
|
+
}), description && /*#__PURE__*/_jsx("div", {
|
|
30
|
+
className: "footer-brand-desc",
|
|
31
|
+
children: description
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
Brand.propTypes = {
|
|
36
|
+
name: PropTypes.node,
|
|
37
|
+
logo: PropTypes.node,
|
|
38
|
+
description: PropTypes.string
|
|
39
|
+
};
|
|
40
|
+
Brand.defaultProps = {
|
|
41
|
+
name: '',
|
|
42
|
+
logo: '',
|
|
43
|
+
description: ''
|
|
44
|
+
};
|
|
45
|
+
const Root = styled('div')`
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
a {
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
color: inherit;
|
|
52
|
+
}
|
|
53
|
+
> div:first-of-type {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
.footer-brand-logo {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
margin-right: 16px;
|
|
61
|
+
line-height: 1;
|
|
62
|
+
img,
|
|
63
|
+
svg {
|
|
64
|
+
width: auto;
|
|
65
|
+
height: 44px;
|
|
66
|
+
max-height: 44px;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
.footer-brand-name {
|
|
70
|
+
font-size: 16px;
|
|
71
|
+
font-weight: bold;
|
|
72
|
+
}
|
|
73
|
+
.footer-brand-desc {
|
|
74
|
+
margin-top: 16px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
${props => props.theme.breakpoints.down('sm')} {
|
|
78
|
+
width: auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
${props => props.theme.breakpoints.down('md')} {
|
|
82
|
+
.footer-brand-logo {
|
|
83
|
+
img,
|
|
84
|
+
svg {
|
|
85
|
+
height: 32px;
|
|
86
|
+
max-height: 32px;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
|
+
export default function Copyright({
|
|
5
|
+
owner,
|
|
6
|
+
year,
|
|
7
|
+
...rest
|
|
8
|
+
}) {
|
|
9
|
+
return /*#__PURE__*/_jsxs(Root, {
|
|
10
|
+
...rest,
|
|
11
|
+
children: ["Copyright \xA9 ", year, " ", owner]
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
Copyright.propTypes = {
|
|
15
|
+
owner: PropTypes.string,
|
|
16
|
+
year: PropTypes.string
|
|
17
|
+
};
|
|
18
|
+
Copyright.defaultProps = {
|
|
19
|
+
owner: 'ArcBlock, Inc.',
|
|
20
|
+
year: `${new Date().getFullYear()}`
|
|
21
|
+
};
|
|
22
|
+
const Root = styled('p')`
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
4
|
+
import { withErrorBoundary } from 'react-error-boundary';
|
|
5
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
7
|
+
import OverridableThemeProvider from '../common/overridable-theme-provider';
|
|
8
|
+
import InternalFooter from './internal-footer';
|
|
9
|
+
import { mapRecursive } from '../utils';
|
|
10
|
+
import { formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
11
|
+
import { blockletMetaProps } from '../types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 专门用于 (composable) blocklet 的 Footer 组件, 基于 blocklet meta 中的数据渲染
|
|
15
|
+
*/
|
|
16
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
17
|
+
function Footer({
|
|
18
|
+
meta,
|
|
19
|
+
theme: themeOverrides,
|
|
20
|
+
...rest
|
|
21
|
+
}) {
|
|
22
|
+
const {
|
|
23
|
+
locale
|
|
24
|
+
} = useLocaleContext() || {};
|
|
25
|
+
const formattedBlocklet = useMemo(() => {
|
|
26
|
+
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
27
|
+
try {
|
|
28
|
+
return formatBlockletInfo(blocklet);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error('Failed to format blocklet info', e, blocklet);
|
|
31
|
+
return blocklet;
|
|
32
|
+
}
|
|
33
|
+
}, [meta]);
|
|
34
|
+
if (!formattedBlocklet.appName) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const {
|
|
38
|
+
appLogo,
|
|
39
|
+
appLogoRect,
|
|
40
|
+
appName,
|
|
41
|
+
appDescription,
|
|
42
|
+
description,
|
|
43
|
+
theme,
|
|
44
|
+
copyright
|
|
45
|
+
} = formattedBlocklet;
|
|
46
|
+
const localized = {
|
|
47
|
+
footerNav: getLocalizedNavigation(formattedBlocklet?.navigation?.footer, locale) || [],
|
|
48
|
+
socialMedia: getLocalizedNavigation(formattedBlocklet?.navigation?.social, locale) || [],
|
|
49
|
+
links: getLocalizedNavigation(formattedBlocklet?.navigation?.bottom, locale) || []
|
|
50
|
+
};
|
|
51
|
+
const props = {
|
|
52
|
+
brand: {
|
|
53
|
+
name: appName,
|
|
54
|
+
description: appDescription || description,
|
|
55
|
+
logo: appLogoRect || appLogo
|
|
56
|
+
},
|
|
57
|
+
navigation: mapRecursive(localized.footerNav, item => ({
|
|
58
|
+
...item,
|
|
59
|
+
label: item.title,
|
|
60
|
+
link: item.link
|
|
61
|
+
}), 'items'),
|
|
62
|
+
copyright,
|
|
63
|
+
socialMedia: localized.socialMedia,
|
|
64
|
+
links: localized.links.map(item => ({
|
|
65
|
+
...item,
|
|
66
|
+
label: item.title
|
|
67
|
+
}))
|
|
68
|
+
};
|
|
69
|
+
return /*#__PURE__*/_jsx(OverridableThemeProvider, {
|
|
70
|
+
theme: themeOverrides,
|
|
71
|
+
children: /*#__PURE__*/_jsx(StyledInternalFooter, {
|
|
72
|
+
...props,
|
|
73
|
+
...rest,
|
|
74
|
+
$bgcolor: theme?.background?.footer
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
Footer.propTypes = {
|
|
79
|
+
meta: blockletMetaProps,
|
|
80
|
+
// 允许覆盖 footer 内置的 theme
|
|
81
|
+
theme: PropTypes.object
|
|
82
|
+
};
|
|
83
|
+
Footer.defaultProps = {
|
|
84
|
+
meta: {},
|
|
85
|
+
theme: null
|
|
86
|
+
};
|
|
87
|
+
const StyledInternalFooter = styled(InternalFooter)`
|
|
88
|
+
border-top: 1px solid #eee;
|
|
89
|
+
color: ${props => props.theme.palette.grey[600]};
|
|
90
|
+
${({
|
|
91
|
+
$bgcolor
|
|
92
|
+
}) => $bgcolor && `background-color: ${$bgcolor};`}
|
|
93
|
+
font-family: Lato, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
|
|
94
|
+
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
95
|
+
`;
|
|
96
|
+
export default withErrorBoundary(Footer, {
|
|
97
|
+
FallbackComponent: ErrorFallback
|
|
98
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import Box from '@mui/material/Box';
|
|
3
|
+
import Brand from './brand';
|
|
4
|
+
import Links from './links';
|
|
5
|
+
import SocialMedia from './social-media';
|
|
6
|
+
import Copyright from './copyright';
|
|
7
|
+
import StandardLayout from './layout/standard';
|
|
8
|
+
import PlainLayout from './layout/plain';
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
const layouts = [{
|
|
12
|
+
name: 'plain',
|
|
13
|
+
// navigation 数据为空时, 使用简单布局
|
|
14
|
+
support: (_, data) => !data.navigation?.length && !data.socialMedia?.length,
|
|
15
|
+
component: PlainLayout
|
|
16
|
+
}, {
|
|
17
|
+
name: 'standard',
|
|
18
|
+
// 默认标准布局
|
|
19
|
+
support: () => true,
|
|
20
|
+
component: StandardLayout
|
|
21
|
+
}];
|
|
22
|
+
const layoutsKeyByName = layouts.reduce((acc, cur) => ({
|
|
23
|
+
...acc,
|
|
24
|
+
[cur.name]: cur
|
|
25
|
+
}), {});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 通用的内部 footer 组件, 定义并渲染常见的几种 footer 元素: brand/navigation/social medial 等
|
|
29
|
+
*/
|
|
30
|
+
function InternalFooter(props) {
|
|
31
|
+
const {
|
|
32
|
+
brand,
|
|
33
|
+
navigation,
|
|
34
|
+
socialMedia,
|
|
35
|
+
copyright,
|
|
36
|
+
links,
|
|
37
|
+
layout,
|
|
38
|
+
...rest
|
|
39
|
+
} = props;
|
|
40
|
+
const renderBrand = () => {
|
|
41
|
+
return brand ? /*#__PURE__*/_jsx(Brand, {
|
|
42
|
+
...brand
|
|
43
|
+
}) : null;
|
|
44
|
+
};
|
|
45
|
+
const renderNavigation = () => {
|
|
46
|
+
return navigation?.length ? /*#__PURE__*/_jsx(Links, {
|
|
47
|
+
links: navigation
|
|
48
|
+
}) : null;
|
|
49
|
+
};
|
|
50
|
+
const renderSocialMedia = () => {
|
|
51
|
+
return socialMedia?.length ? /*#__PURE__*/_jsx(SocialMedia, {
|
|
52
|
+
items: socialMedia
|
|
53
|
+
}) : null;
|
|
54
|
+
};
|
|
55
|
+
const renderCopyright = () => {
|
|
56
|
+
// 如果 copyright.owner 不存在, 则使用 brand.name, 如果 brand.name 也不存在, copyright 元素为空
|
|
57
|
+
const copyrightOwner = copyright?.owner || brand?.name;
|
|
58
|
+
if (!copyrightOwner) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return /*#__PURE__*/_jsx(Copyright, {
|
|
62
|
+
owner: copyrightOwner,
|
|
63
|
+
year: copyright?.year || undefined
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const renderLinks = () => {
|
|
67
|
+
return links?.length ? /*#__PURE__*/_jsx(Links, {
|
|
68
|
+
flowLayout: true,
|
|
69
|
+
links: links
|
|
70
|
+
}) : null;
|
|
71
|
+
};
|
|
72
|
+
const elements = {
|
|
73
|
+
brand: renderBrand(),
|
|
74
|
+
navigation: renderNavigation(),
|
|
75
|
+
socialMedia: renderSocialMedia(),
|
|
76
|
+
copyright: renderCopyright(),
|
|
77
|
+
links: renderLinks()
|
|
78
|
+
};
|
|
79
|
+
let LayoutComponent = null;
|
|
80
|
+
if (layout === 'auto') {
|
|
81
|
+
LayoutComponent = layouts.find(item => item.support(elements, props)).component;
|
|
82
|
+
} else {
|
|
83
|
+
LayoutComponent = layoutsKeyByName[layout]?.component;
|
|
84
|
+
}
|
|
85
|
+
if (!LayoutComponent) {
|
|
86
|
+
throw new Error(`layout ${layout} is not supported.`);
|
|
87
|
+
}
|
|
88
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
89
|
+
position: "relative",
|
|
90
|
+
...rest,
|
|
91
|
+
children: [/*#__PURE__*/_jsx(LayoutComponent, {
|
|
92
|
+
elements: elements,
|
|
93
|
+
data: props
|
|
94
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
95
|
+
position: "absolute",
|
|
96
|
+
right: 16,
|
|
97
|
+
bottom: 0,
|
|
98
|
+
fontSize: 12,
|
|
99
|
+
sx: {
|
|
100
|
+
color: 'transparent',
|
|
101
|
+
'::selection': {
|
|
102
|
+
background: '#000',
|
|
103
|
+
color: '#fff'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
children: window?.blocklet?.version
|
|
107
|
+
})]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
InternalFooter.propTypes = {
|
|
111
|
+
brand: PropTypes.shape({
|
|
112
|
+
name: PropTypes.node,
|
|
113
|
+
description: PropTypes.string,
|
|
114
|
+
logo: PropTypes.node
|
|
115
|
+
}),
|
|
116
|
+
navigation: PropTypes.arrayOf(PropTypes.shape({
|
|
117
|
+
label: PropTypes.node,
|
|
118
|
+
link: PropTypes.string
|
|
119
|
+
})),
|
|
120
|
+
socialMedia: PropTypes.arrayOf(PropTypes.shape({
|
|
121
|
+
icon: PropTypes.node,
|
|
122
|
+
link: PropTypes.string
|
|
123
|
+
})),
|
|
124
|
+
copyright: PropTypes.shape({
|
|
125
|
+
owner: PropTypes.string,
|
|
126
|
+
year: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
|
127
|
+
}),
|
|
128
|
+
// privacy/legal 等链接, 常放于 footer 右下侧或最底部
|
|
129
|
+
links: PropTypes.arrayOf(PropTypes.shape({
|
|
130
|
+
label: PropTypes.node,
|
|
131
|
+
link: PropTypes.string
|
|
132
|
+
})),
|
|
133
|
+
// 可显式指定 footer layout, 默认根据内容自动决定 layout
|
|
134
|
+
layout: PropTypes.oneOf(['auto', 'standard', 'plain'])
|
|
135
|
+
};
|
|
136
|
+
InternalFooter.defaultProps = {
|
|
137
|
+
brand: null,
|
|
138
|
+
navigation: null,
|
|
139
|
+
copyright: null,
|
|
140
|
+
socialMedia: null,
|
|
141
|
+
links: null,
|
|
142
|
+
layout: 'auto'
|
|
143
|
+
};
|
|
144
|
+
export default InternalFooter;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cloneElement } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Container from '@mui/material/Container';
|
|
4
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
5
|
+
import Row from './row';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* footer plain layout
|
|
9
|
+
*/
|
|
10
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { Fragment as _Fragment } from "react/jsx-runtime";
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
function PlainLayout({
|
|
14
|
+
elements,
|
|
15
|
+
data,
|
|
16
|
+
...rest
|
|
17
|
+
}) {
|
|
18
|
+
return /*#__PURE__*/_jsx(Root, {
|
|
19
|
+
...rest,
|
|
20
|
+
children: /*#__PURE__*/_jsxs(Container, {
|
|
21
|
+
className: "plain-layout-container",
|
|
22
|
+
children: [!!data.links && /*#__PURE__*/_jsxs(Row, {
|
|
23
|
+
sx: {
|
|
24
|
+
width: 1
|
|
25
|
+
},
|
|
26
|
+
autoCenter: true,
|
|
27
|
+
children: [elements.copyright, elements.links]
|
|
28
|
+
}), !data.links && /*#__PURE__*/_jsxs(_Fragment, {
|
|
29
|
+
children: [/*#__PURE__*/cloneElement(elements.brand, {
|
|
30
|
+
name: null,
|
|
31
|
+
description: null
|
|
32
|
+
}), elements.copyright]
|
|
33
|
+
})]
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
PlainLayout.propTypes = {
|
|
38
|
+
elements: PropTypes.shape({
|
|
39
|
+
brand: PropTypes.element,
|
|
40
|
+
navigation: PropTypes.element,
|
|
41
|
+
socialMedia: PropTypes.element,
|
|
42
|
+
copyright: PropTypes.element,
|
|
43
|
+
links: PropTypes.element
|
|
44
|
+
}).isRequired,
|
|
45
|
+
data: PropTypes.object.isRequired
|
|
46
|
+
};
|
|
47
|
+
const Root = styled('div')`
|
|
48
|
+
padding: 24px 0;
|
|
49
|
+
.plain-layout-container {
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
align-items: center;
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
export default PlainLayout;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import Box from '@mui/material/Box';
|
|
3
|
+
import { styled } from '@arcblock/ux/lib/Theme';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
export default function Row({
|
|
7
|
+
children,
|
|
8
|
+
autoCenter,
|
|
9
|
+
...rest
|
|
10
|
+
}) {
|
|
11
|
+
if (!children) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return /*#__PURE__*/_jsx(RowRoot, {
|
|
15
|
+
...rest,
|
|
16
|
+
className: clsx(rest.className, {
|
|
17
|
+
'footer-row-auto-center': autoCenter
|
|
18
|
+
}),
|
|
19
|
+
children: children
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
Row.propTypes = {
|
|
23
|
+
children: PropTypes.any,
|
|
24
|
+
autoCenter: PropTypes.bool
|
|
25
|
+
};
|
|
26
|
+
Row.defaultProps = {
|
|
27
|
+
children: null,
|
|
28
|
+
autoCenter: false
|
|
29
|
+
};
|
|
30
|
+
const RowRoot = styled(Box)`
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
& + & {
|
|
34
|
+
margin-top: 24px;
|
|
35
|
+
}
|
|
36
|
+
&.footer-row-auto-center > *:only-child {
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
${props => props.theme.breakpoints.down('md')} {
|
|
41
|
+
align-items: stretch;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 16px;
|
|
44
|
+
> * {
|
|
45
|
+
flex: 1 0 100%;
|
|
46
|
+
}
|
|
47
|
+
&.footer-row-auto-center > * {
|
|
48
|
+
margin: 0 auto;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|