@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,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
+ `;