@blocklet/ui-react 2.12.0 → 2.12.2
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 +7 -5
- package/lib/Footer/index.js +23 -1
- package/lib/Footer/internal-footer.js +1 -1
- package/lib/Footer/layout/standard.d.ts +3 -1
- package/lib/Footer/layout/standard.js +43 -6
- package/lib/Footer/links.d.ts +3 -1
- package/lib/Footer/links.js +102 -44
- package/lib/Footer/social-media.js +2 -1
- package/lib/Header/index.js +23 -1
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +71 -0
- package/package.json +4 -4
- package/src/Footer/brand.jsx +7 -5
- package/src/Footer/index.jsx +27 -1
- package/src/Footer/internal-footer.jsx +1 -1
- package/src/Footer/layout/standard.jsx +48 -10
- package/src/Footer/links.jsx +130 -54
- package/src/Footer/social-media.jsx +2 -1
- package/src/Header/index.tsx +26 -1
- package/src/utils.js +71 -0
package/lib/Footer/brand.js
CHANGED
|
@@ -41,21 +41,23 @@ const Root = styled("div")`
|
|
|
41
41
|
.footer-brand-logo {
|
|
42
42
|
display: flex;
|
|
43
43
|
align-items: center;
|
|
44
|
-
margin-right:
|
|
44
|
+
margin-right: 12px;
|
|
45
45
|
line-height: 1;
|
|
46
46
|
img,
|
|
47
47
|
svg {
|
|
48
48
|
width: auto;
|
|
49
|
-
height:
|
|
50
|
-
max-height:
|
|
49
|
+
height: 40px;
|
|
50
|
+
max-height: 40px;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
.footer-brand-name {
|
|
54
|
-
font-size:
|
|
55
|
-
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
color: ${(props) => props.theme.palette.grey[900]};
|
|
56
56
|
}
|
|
57
57
|
.footer-brand-desc {
|
|
58
|
+
white-space: pre-line;
|
|
58
59
|
margin-top: 16px;
|
|
60
|
+
color: #9397a1;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
${(props) => props.theme.breakpoints.down("sm")} {
|
package/lib/Footer/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
|
+
import { useCreation } from "ahooks";
|
|
4
5
|
import { styled } from "@arcblock/ux/lib/Theme";
|
|
5
6
|
import { withErrorBoundary } from "react-error-boundary";
|
|
6
7
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
7
8
|
import { ErrorFallback } from "@arcblock/ux/lib/ErrorBoundary";
|
|
8
9
|
import { temp as colors } from "@arcblock/ux/lib/Colors";
|
|
9
10
|
import omit from "lodash/omit";
|
|
11
|
+
import isFinite from "lodash/isFinite";
|
|
10
12
|
import OverridableThemeProvider from "../common/overridable-theme-provider.js";
|
|
11
13
|
import InternalFooter from "./internal-footer.js";
|
|
12
14
|
import { mapRecursive } from "../utils.js";
|
|
@@ -24,12 +26,32 @@ function Footer({ meta, theme: themeOverrides, ...rest }) {
|
|
|
24
26
|
return blocklet;
|
|
25
27
|
}
|
|
26
28
|
}, [meta]);
|
|
29
|
+
const productsNav = useCreation(() => {
|
|
30
|
+
return {
|
|
31
|
+
title: { en: "Products", zh: "\u4EA7\u54C1" },
|
|
32
|
+
section: ["footer"],
|
|
33
|
+
items: [
|
|
34
|
+
{ title: "ArcSphere", link: `https://www.arcblock.io/content/tags/${locale}/arcsphere`, isNew: true },
|
|
35
|
+
{ title: "DID Wallet", link: `https://www.didwallet.io/${locale}` },
|
|
36
|
+
{ title: "DID Spaces", link: `https://www.didspaces.com/${locale}` },
|
|
37
|
+
{ title: "DID Name Service", link: `https://www.didnames.io/${locale}` },
|
|
38
|
+
{ title: "Blocklet Launcher", link: `https://launcher.arcblock.io/${locale}` },
|
|
39
|
+
{ title: "Blocklet Server", link: `https://www.arcblock.io/content/collections/${locale}/blocklet-server` },
|
|
40
|
+
{ title: "AIGNE", link: `https://www.aigne.io/${locale}` }
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}, [locale]);
|
|
27
44
|
if (!formattedBlocklet.appName) {
|
|
28
45
|
return null;
|
|
29
46
|
}
|
|
30
47
|
const { appLogo, appLogoRect, appName, appDescription, description, theme, copyright } = formattedBlocklet;
|
|
48
|
+
const navFooter = [...formattedBlocklet?.navigation?.footer ?? []];
|
|
49
|
+
const productsNavOrder = parseInt(window.blocklet?.USE_ARCBLOCK_THEME, 10);
|
|
50
|
+
if (isFinite(productsNavOrder)) {
|
|
51
|
+
navFooter.splice(productsNavOrder, 0, productsNav);
|
|
52
|
+
}
|
|
31
53
|
const localized = {
|
|
32
|
-
footerNav: getLocalizedNavigation(
|
|
54
|
+
footerNav: getLocalizedNavigation(navFooter, locale) || [],
|
|
33
55
|
socialMedia: getLocalizedNavigation(formattedBlocklet?.navigation?.social, locale) || [],
|
|
34
56
|
links: getLocalizedNavigation(formattedBlocklet?.navigation?.bottom, locale) || []
|
|
35
57
|
};
|
|
@@ -29,7 +29,7 @@ function InternalFooter(props) {
|
|
|
29
29
|
return brand ? /* @__PURE__ */ jsx(Brand, { ...brand }) : null;
|
|
30
30
|
};
|
|
31
31
|
const renderNavigation = () => {
|
|
32
|
-
return navigation?.length ? /* @__PURE__ */ jsx(Links, { links: navigation }) : null;
|
|
32
|
+
return navigation?.length ? /* @__PURE__ */ jsx(Links, { links: navigation, columns: 3 }) : null;
|
|
33
33
|
};
|
|
34
34
|
const renderSocialMedia = () => {
|
|
35
35
|
return socialMedia?.length ? /* @__PURE__ */ jsx(SocialMedia, { items: socialMedia }) : null;
|
|
@@ -2,14 +2,16 @@ export default StandardLayout;
|
|
|
2
2
|
/**
|
|
3
3
|
* footer standard layout
|
|
4
4
|
*/
|
|
5
|
-
declare function StandardLayout({ elements, data, ...rest }: {
|
|
5
|
+
declare function StandardLayout({ elements, data, className, ...rest }: {
|
|
6
6
|
[x: string]: any;
|
|
7
7
|
elements: any;
|
|
8
8
|
data: any;
|
|
9
|
+
className: any;
|
|
9
10
|
}): import("react").JSX.Element;
|
|
10
11
|
declare namespace StandardLayout {
|
|
11
12
|
namespace propTypes {
|
|
12
13
|
let elements: any;
|
|
13
14
|
let data: any;
|
|
15
|
+
let className: any;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
|
+
import clsx from "clsx";
|
|
3
4
|
import Box from "@mui/material/Box";
|
|
5
|
+
import { grey } from "@mui/material/colors";
|
|
4
6
|
import Container from "@mui/material/Container";
|
|
5
7
|
import { styled } from "@arcblock/ux/lib/Theme";
|
|
6
8
|
import Row from "./row.js";
|
|
7
|
-
function StandardLayout({ elements, data, ...rest }) {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
function StandardLayout({ elements, data, className, ...rest }) {
|
|
10
|
+
const withNavigation = !!data.navigation?.length;
|
|
11
|
+
let topSection = null;
|
|
12
|
+
if (withNavigation) {
|
|
13
|
+
topSection = /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: { xs: "column", md: "row" }, justifyContent: "space-between" }, children: [
|
|
14
|
+
/* @__PURE__ */ jsxs(
|
|
15
|
+
Box,
|
|
16
|
+
{
|
|
17
|
+
sx: {
|
|
18
|
+
flex: "1 1 auto",
|
|
19
|
+
paddingRight: { xs: 0, md: 3 },
|
|
20
|
+
display: "flex",
|
|
21
|
+
flexDirection: "column",
|
|
22
|
+
alignItems: { xs: "center", md: "flex-start" },
|
|
23
|
+
gap: 2,
|
|
24
|
+
pb: 3
|
|
25
|
+
},
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx(Box, { children: elements.brand }),
|
|
28
|
+
/* @__PURE__ */ jsx(Box, { lineHeight: 1, children: elements.socialMedia })
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
/* @__PURE__ */ jsx(Box, { sx: { mb: 3, borderTop: { xs: `1px solid ${grey[200]}`, md: 0 } }, children: elements.navigation })
|
|
33
|
+
] });
|
|
34
|
+
} else {
|
|
35
|
+
topSection = /* @__PURE__ */ jsxs(
|
|
10
36
|
Box,
|
|
11
37
|
{
|
|
12
38
|
sx: {
|
|
@@ -22,8 +48,10 @@ function StandardLayout({ elements, data, ...rest }) {
|
|
|
22
48
|
/* @__PURE__ */ jsx(Box, { lineHeight: 1, children: elements.socialMedia })
|
|
23
49
|
]
|
|
24
50
|
}
|
|
25
|
-
)
|
|
26
|
-
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return /* @__PURE__ */ jsx(Root, { ...rest, className: clsx({ "footer--with-navs": withNavigation }, className), children: /* @__PURE__ */ jsxs(Container, { children: [
|
|
54
|
+
topSection,
|
|
27
55
|
/* @__PURE__ */ jsxs(Row, { sx: { pt: 3, borderTop: 1, borderColor: "grey.200" }, autoCenter: true, children: [
|
|
28
56
|
elements.copyright,
|
|
29
57
|
elements.links
|
|
@@ -38,7 +66,8 @@ StandardLayout.propTypes = {
|
|
|
38
66
|
copyright: PropTypes.element,
|
|
39
67
|
links: PropTypes.element
|
|
40
68
|
}).isRequired,
|
|
41
|
-
data: PropTypes.object.isRequired
|
|
69
|
+
data: PropTypes.object.isRequired,
|
|
70
|
+
className: PropTypes.string
|
|
42
71
|
};
|
|
43
72
|
const Root = styled("div")`
|
|
44
73
|
padding: 32px 0 24px 0;
|
|
@@ -46,6 +75,14 @@ const Root = styled("div")`
|
|
|
46
75
|
.footer-brand-desc {
|
|
47
76
|
display: none;
|
|
48
77
|
}
|
|
78
|
+
&.footer--with-navs {
|
|
79
|
+
${(props) => props.theme.breakpoints.up("md")} {
|
|
80
|
+
.footer-brand-desc {
|
|
81
|
+
max-width: 360px;
|
|
82
|
+
display: block;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
49
86
|
&& .footer-brand-logo {
|
|
50
87
|
margin-right: 0;
|
|
51
88
|
}
|
package/lib/Footer/links.d.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* footer 中的 links (支持分组, 最多支持 2 级)
|
|
3
3
|
* TODO: dark/light theme
|
|
4
4
|
*/
|
|
5
|
-
declare function Links({ links, flowLayout, ...rest }: {
|
|
5
|
+
declare function Links({ links, flowLayout, columns, ...rest }: {
|
|
6
6
|
[x: string]: any;
|
|
7
7
|
links: any;
|
|
8
8
|
flowLayout: any;
|
|
9
|
+
columns: any;
|
|
9
10
|
}): import("react").JSX.Element | null;
|
|
10
11
|
declare namespace Links {
|
|
11
12
|
namespace propTypes {
|
|
12
13
|
let links: any;
|
|
13
14
|
let flowLayout: any;
|
|
15
|
+
let columns: any;
|
|
14
16
|
}
|
|
15
17
|
namespace defaultProps {
|
|
16
18
|
let links_1: never[];
|
package/lib/Footer/links.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
|
+
import { useCreation } from "ahooks";
|
|
5
|
+
import isInteger from "lodash/isInteger";
|
|
4
6
|
import { styled } from "@arcblock/ux/lib/Theme";
|
|
5
7
|
import clsx from "clsx";
|
|
6
8
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
7
9
|
import Icon from "../Icon/index.js";
|
|
8
|
-
|
|
10
|
+
import useMobile from "../hooks/use-mobile.js";
|
|
11
|
+
import { splitNavColumns } from "../utils.js";
|
|
12
|
+
export default function Links({ links, flowLayout, columns, ...rest }) {
|
|
9
13
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
10
|
-
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
14
|
+
const isMobile = useMobile({ key: "md" });
|
|
13
15
|
const isGroupMode = links.some((item) => item.items?.length);
|
|
16
|
+
const columnsLayout = !isMobile && isGroupMode && isInteger(columns) && columns > 1;
|
|
14
17
|
const renderItem = ({ label, link, icon, render, props }) => {
|
|
15
18
|
let result = label;
|
|
16
19
|
if (render) {
|
|
@@ -23,45 +26,73 @@ export default function Links({ links, flowLayout, ...rest }) {
|
|
|
23
26
|
result
|
|
24
27
|
] });
|
|
25
28
|
};
|
|
29
|
+
const content = useCreation(() => {
|
|
30
|
+
if (!links?.length) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (flowLayout) {
|
|
34
|
+
return links.map((item, i) => /* @__PURE__ */ jsx("span", { className: "footer-links-item", children: renderItem(item) }, i));
|
|
35
|
+
}
|
|
36
|
+
if (columnsLayout) {
|
|
37
|
+
return splitNavColumns(links, { columns }).map((cols, i) => {
|
|
38
|
+
return /* @__PURE__ */ jsx("div", { className: "footer-links-column", children: cols.filter((v) => v.group).map((item, j) => {
|
|
39
|
+
const { items } = item;
|
|
40
|
+
return /* @__PURE__ */ jsxs("div", { className: "footer-links-group", children: [
|
|
41
|
+
/* @__PURE__ */ jsx("span", { className: "footer-links-item", children: renderItem(item) }),
|
|
42
|
+
!!items?.length && /* @__PURE__ */ jsx("div", { className: "footer-links-sub", children: items.map((child, k) => /* @__PURE__ */ jsx(
|
|
43
|
+
"span",
|
|
44
|
+
{
|
|
45
|
+
className: clsx("footer-links-item", { "footer-links-item--new": child.isNew }),
|
|
46
|
+
children: renderItem(child)
|
|
47
|
+
},
|
|
48
|
+
k
|
|
49
|
+
)) })
|
|
50
|
+
] }, j);
|
|
51
|
+
}) }, i);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return links.map((item, i) => {
|
|
55
|
+
const { items } = item;
|
|
56
|
+
const isActive = i === activeIndex;
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
"div",
|
|
59
|
+
{
|
|
60
|
+
className: clsx("footer-links-group", {
|
|
61
|
+
"footer-links-group--active": isActive
|
|
62
|
+
}),
|
|
63
|
+
onClick: () => setActiveIndex(activeIndex === i ? -1 : i),
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("span", { className: "footer-links-item", children: [
|
|
66
|
+
renderItem(item),
|
|
67
|
+
!!items?.length && /* @__PURE__ */ jsx("span", { className: "footer-links-group-expand-icon", children: /* @__PURE__ */ jsx(
|
|
68
|
+
ExpandMoreIcon,
|
|
69
|
+
{
|
|
70
|
+
style: {
|
|
71
|
+
transform: `rotate(${isActive ? 180 : 0}deg)`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
) })
|
|
75
|
+
] }),
|
|
76
|
+
!!items?.length && /* @__PURE__ */ jsx("div", { className: "footer-links-sub", children: items.map((child, j) => /* @__PURE__ */ jsx("span", { className: clsx("footer-links-item", { "footer-links-item--new": child.isNew }), children: renderItem(child) }, j)) })
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
i
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}, [links, flowLayout, columnsLayout, activeIndex]);
|
|
83
|
+
if (!links?.length) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
26
86
|
return /* @__PURE__ */ jsx(
|
|
27
87
|
Root,
|
|
28
88
|
{
|
|
29
89
|
...rest,
|
|
30
90
|
className: clsx(rest.className, {
|
|
31
91
|
"footer-links--grouped": isGroupMode,
|
|
32
|
-
"footer-links--flow": flowLayout
|
|
92
|
+
"footer-links--flow": flowLayout,
|
|
93
|
+
"footer-links--columns": columnsLayout
|
|
33
94
|
}),
|
|
34
|
-
children: /* @__PURE__ */
|
|
35
|
-
flowLayout && links.map((item, i) => /* @__PURE__ */ jsx("span", { className: "footer-links-item", children: renderItem(item) }, i)),
|
|
36
|
-
!flowLayout && links.map((item, i) => {
|
|
37
|
-
const { items } = item;
|
|
38
|
-
const isActive = i === activeIndex;
|
|
39
|
-
return /* @__PURE__ */ jsxs(
|
|
40
|
-
"div",
|
|
41
|
-
{
|
|
42
|
-
className: clsx("footer-links-group", {
|
|
43
|
-
"footer-links-group--active": isActive
|
|
44
|
-
}),
|
|
45
|
-
onClick: () => setActiveIndex(activeIndex === i ? -1 : i),
|
|
46
|
-
children: [
|
|
47
|
-
/* @__PURE__ */ jsxs("span", { className: "footer-links-item", children: [
|
|
48
|
-
renderItem(item),
|
|
49
|
-
!!items?.length && /* @__PURE__ */ jsx("span", { className: "footer-links-group-expand-icon", children: /* @__PURE__ */ jsx(
|
|
50
|
-
ExpandMoreIcon,
|
|
51
|
-
{
|
|
52
|
-
style: {
|
|
53
|
-
transform: `rotate(${isActive ? 180 : 0}deg)`
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
) })
|
|
57
|
-
] }),
|
|
58
|
-
!!items?.length && /* @__PURE__ */ jsx("div", { className: "footer-links-sub", children: items.map((child, j) => /* @__PURE__ */ jsx("span", { className: "footer-links-item", children: renderItem(child) }, j)) })
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
i
|
|
62
|
-
);
|
|
63
|
-
})
|
|
64
|
-
] })
|
|
95
|
+
children: /* @__PURE__ */ jsx("div", { className: "footer-links-inner", children: content })
|
|
65
96
|
}
|
|
66
97
|
);
|
|
67
98
|
}
|
|
@@ -75,7 +106,9 @@ Links.propTypes = {
|
|
|
75
106
|
})
|
|
76
107
|
),
|
|
77
108
|
// 流动布局, 简单的从左到右排列
|
|
78
|
-
flowLayout: PropTypes.bool
|
|
109
|
+
flowLayout: PropTypes.bool,
|
|
110
|
+
// 列布局
|
|
111
|
+
columns: PropTypes.number
|
|
79
112
|
};
|
|
80
113
|
Links.defaultProps = {
|
|
81
114
|
links: [],
|
|
@@ -83,7 +116,7 @@ Links.defaultProps = {
|
|
|
83
116
|
};
|
|
84
117
|
const Root = styled("div")`
|
|
85
118
|
overflow: hidden;
|
|
86
|
-
color:
|
|
119
|
+
color: #9397a1;
|
|
87
120
|
.footer-links-inner {
|
|
88
121
|
display: flex;
|
|
89
122
|
justify-content: space-between;
|
|
@@ -94,9 +127,6 @@ const Root = styled("div")`
|
|
|
94
127
|
display: flex;
|
|
95
128
|
flex-direction: column;
|
|
96
129
|
}
|
|
97
|
-
.footer-links-sub .footer-links-item {
|
|
98
|
-
color: ${(props) => props.theme.palette.grey[900]};
|
|
99
|
-
}
|
|
100
130
|
.footer-links-group-expand-icon {
|
|
101
131
|
display: none;
|
|
102
132
|
position: absolute;
|
|
@@ -113,13 +143,22 @@ const Root = styled("div")`
|
|
|
113
143
|
display: inline-flex;
|
|
114
144
|
align-items: center;
|
|
115
145
|
position: relative;
|
|
116
|
-
padding:
|
|
146
|
+
padding: 6px 8px;
|
|
117
147
|
font-size: 14px;
|
|
148
|
+
&--new::after {
|
|
149
|
+
content: 'New';
|
|
150
|
+
color: #4672ea;
|
|
151
|
+
background-color: #e1e8fb;
|
|
152
|
+
padding: 1px 8px;
|
|
153
|
+
border-radius: 10px/50%;
|
|
154
|
+
margin-left: 8px;
|
|
155
|
+
}
|
|
118
156
|
}
|
|
119
157
|
&.footer-links--grouped {
|
|
120
158
|
.footer-links-group {
|
|
121
159
|
> .footer-links-item {
|
|
122
|
-
font-weight:
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
color: #25292f;
|
|
123
162
|
}
|
|
124
163
|
.footer-links-sub {
|
|
125
164
|
margin-top: 8px;
|
|
@@ -131,11 +170,29 @@ const Root = styled("div")`
|
|
|
131
170
|
max-width: 150px;
|
|
132
171
|
color: inherit;
|
|
133
172
|
text-decoration: none;
|
|
173
|
+
transition: color 0.2s ease-in-out;
|
|
134
174
|
&:hover {
|
|
135
|
-
|
|
175
|
+
color: #25292f;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/* columns 布局 */
|
|
179
|
+
&.footer-links--columns {
|
|
180
|
+
.footer-links-inner {
|
|
181
|
+
gap: 96px;
|
|
182
|
+
}
|
|
183
|
+
.footer-links-column {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
}
|
|
187
|
+
.footer-links-group {
|
|
188
|
+
.footer-links-sub {
|
|
189
|
+
margin-top: 2px;
|
|
190
|
+
margin-bottom: 12px;
|
|
191
|
+
}
|
|
136
192
|
}
|
|
137
193
|
}
|
|
138
194
|
|
|
195
|
+
/* flow 布局 */
|
|
139
196
|
&.footer-links--flow {
|
|
140
197
|
display: inline-flex;
|
|
141
198
|
.footer-links-inner {
|
|
@@ -157,6 +214,7 @@ const Root = styled("div")`
|
|
|
157
214
|
}
|
|
158
215
|
}
|
|
159
216
|
|
|
217
|
+
/* 移动端样式 */
|
|
160
218
|
${(props) => props.theme.breakpoints.down("md")} {
|
|
161
219
|
.footer-links-inner {
|
|
162
220
|
flex-direction: column;
|
|
@@ -50,8 +50,9 @@ const Root = styled("div")`
|
|
|
50
50
|
a {
|
|
51
51
|
color: ${(props) => props.theme.palette.grey[400]};
|
|
52
52
|
text-decoration: none;
|
|
53
|
+
transition: color 0.2s ease-in-out;
|
|
53
54
|
&:hover {
|
|
54
|
-
color:
|
|
55
|
+
color: #25292f;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
${(props) => props.theme.breakpoints.down("md")} {
|
package/lib/Header/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
|
+
import { useMemoizedFn } from "ahooks";
|
|
3
4
|
import { withErrorBoundary } from "react-error-boundary";
|
|
4
5
|
import { ErrorFallback } from "@arcblock/ux/lib/ErrorBoundary";
|
|
5
6
|
import { styled } from "@arcblock/ux/lib/Theme";
|
|
6
7
|
import { ResponsiveHeader } from "@arcblock/ux/lib/Header";
|
|
7
|
-
import NavMenu from "@arcblock/ux/lib/NavMenu";
|
|
8
|
+
import NavMenu, { Products } from "@arcblock/ux/lib/NavMenu";
|
|
8
9
|
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
|
|
9
10
|
import { temp as colors } from "@arcblock/ux/lib/Colors";
|
|
11
|
+
import { translate } from "@arcblock/ux/lib/Locale/util";
|
|
10
12
|
import omit from "lodash/omit";
|
|
13
|
+
import isFinite from "lodash/isFinite";
|
|
11
14
|
import clsx from "clsx";
|
|
12
15
|
import Icon from "../Icon/index.js";
|
|
13
16
|
import OverridableThemeProvider from "../common/overridable-theme-provider.js";
|
|
@@ -17,6 +20,14 @@ import HeaderAddons from "../common/header-addons.js";
|
|
|
17
20
|
import { useWalletHiddenTopbar } from "../common/wallet-hidden-topbar.js";
|
|
18
21
|
import withHideWhenEmbed from "../libs/with-hide-when-embed.js";
|
|
19
22
|
import useMobile from "../hooks/use-mobile.js";
|
|
23
|
+
const translations = {
|
|
24
|
+
en: {
|
|
25
|
+
products: "Products"
|
|
26
|
+
},
|
|
27
|
+
zh: {
|
|
28
|
+
products: "\u4EA7\u54C1"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
20
31
|
const parseNavigation = (navigation) => {
|
|
21
32
|
if (!navigation?.length) {
|
|
22
33
|
return { navItems: [], activeId: null };
|
|
@@ -65,6 +76,9 @@ function Header({
|
|
|
65
76
|
}) {
|
|
66
77
|
useWalletHiddenTopbar();
|
|
67
78
|
const { locale } = useLocaleContext() || {};
|
|
79
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
80
|
+
return translate(translations, key, locale, "en", data);
|
|
81
|
+
});
|
|
68
82
|
const formattedBlocklet = useMemo(() => {
|
|
69
83
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
70
84
|
try {
|
|
@@ -82,6 +96,14 @@ function Header({
|
|
|
82
96
|
const navigation = getLocalizedNavigation(formattedBlocklet?.navigation?.header, locale);
|
|
83
97
|
const parsedNavigation = parseNavigation(navigation);
|
|
84
98
|
const { navItems, activeId } = parsedNavigation;
|
|
99
|
+
const productsNavOrder = parseInt(window.blocklet?.USE_ARCBLOCK_THEME, 10);
|
|
100
|
+
if (isFinite(productsNavOrder)) {
|
|
101
|
+
navItems.splice(productsNavOrder, 0, {
|
|
102
|
+
label: t("products"),
|
|
103
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
104
|
+
children: ({ isOpen }) => /* @__PURE__ */ jsx(Products, { isOpen })
|
|
105
|
+
});
|
|
106
|
+
}
|
|
85
107
|
const _addons = typeof addons === "function" ? (builtInAddons) => addons(builtInAddons, { navigation: parsedNavigation }) : addons;
|
|
86
108
|
const headerAddons = (
|
|
87
109
|
// @ts-ignore
|
package/lib/utils.d.ts
CHANGED
package/lib/utils.js
CHANGED
|
@@ -80,3 +80,74 @@ export const matchPaths = (paths = []) => {
|
|
|
80
80
|
}, matched[0]);
|
|
81
81
|
return mostSpecific.index;
|
|
82
82
|
};
|
|
83
|
+
|
|
84
|
+
/** 导航列表分列 */
|
|
85
|
+
export const splitNavColumns = (items, options = {}) => {
|
|
86
|
+
const { columns = 1, breakInside = false, groupHeight = 48, itemHeight = 24, childrenKey = 'items' } = options;
|
|
87
|
+
|
|
88
|
+
// 高度预估
|
|
89
|
+
const totalHeight = items.reduce((height, group) => {
|
|
90
|
+
return height + groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
|
|
91
|
+
}, 0);
|
|
92
|
+
const targetHeight = Math.ceil(totalHeight / columns);
|
|
93
|
+
|
|
94
|
+
// 使用贪心策略进行分列
|
|
95
|
+
const result = [[]];
|
|
96
|
+
let currentColumn = 0;
|
|
97
|
+
let currentHeight = 0;
|
|
98
|
+
|
|
99
|
+
// 允许的高度偏差范围(有利于得到高度相差不大的列)
|
|
100
|
+
const heightVariance = targetHeight * 0.2;
|
|
101
|
+
|
|
102
|
+
// 是否应该分列
|
|
103
|
+
const shouldBreakColumn = (nextHeight) => {
|
|
104
|
+
return (
|
|
105
|
+
currentHeight > targetHeight - heightVariance &&
|
|
106
|
+
currentColumn < columns - 1 &&
|
|
107
|
+
currentHeight + nextHeight > targetHeight + heightVariance
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
items.forEach((group) => {
|
|
112
|
+
const groupTotalHeight = groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
|
|
113
|
+
|
|
114
|
+
// 允许截断分组时,可以在任何子项处换列
|
|
115
|
+
if (breakInside && shouldBreakColumn(groupHeight)) {
|
|
116
|
+
currentColumn++;
|
|
117
|
+
currentHeight = 0;
|
|
118
|
+
result[currentColumn] = [];
|
|
119
|
+
}
|
|
120
|
+
// 不允许截断分组时,只能在分组边界换列
|
|
121
|
+
if (!breakInside && currentHeight > 0 && shouldBreakColumn(groupTotalHeight)) {
|
|
122
|
+
currentColumn++;
|
|
123
|
+
currentHeight = 0;
|
|
124
|
+
result[currentColumn] = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 添加分组标题
|
|
128
|
+
result[currentColumn].push({
|
|
129
|
+
...group,
|
|
130
|
+
group: true,
|
|
131
|
+
});
|
|
132
|
+
currentHeight += groupHeight;
|
|
133
|
+
|
|
134
|
+
// 添加子项
|
|
135
|
+
if (group[childrenKey]) {
|
|
136
|
+
group[childrenKey].forEach((child) => {
|
|
137
|
+
if (breakInside && shouldBreakColumn(itemHeight)) {
|
|
138
|
+
currentColumn++;
|
|
139
|
+
currentHeight = 0;
|
|
140
|
+
result[currentColumn] = [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
result[currentColumn].push({
|
|
144
|
+
...child,
|
|
145
|
+
group: false,
|
|
146
|
+
});
|
|
147
|
+
currentHeight += itemHeight;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/ui-react",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"description": "Some useful front-end web components that can be used in Blocklets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@abtnode/constant": "^1.16.39",
|
|
36
|
-
"@arcblock/bridge": "^2.12.
|
|
37
|
-
"@arcblock/react-hooks": "^2.12.
|
|
36
|
+
"@arcblock/bridge": "^2.12.2",
|
|
37
|
+
"@arcblock/react-hooks": "^2.12.2",
|
|
38
38
|
"@arcblock/ws": "^1.19.13",
|
|
39
39
|
"@blocklet/did-space-react": "^1.0.22",
|
|
40
40
|
"@iconify-icons/logos": "^1.2.36",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"jest": "^29.7.0",
|
|
85
85
|
"unbuild": "^2.0.0"
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "4dc132cab82765eef5194cf00075a13bd5d8e458"
|
|
88
88
|
}
|
package/src/Footer/brand.jsx
CHANGED
|
@@ -52,21 +52,23 @@ const Root = styled('div')`
|
|
|
52
52
|
.footer-brand-logo {
|
|
53
53
|
display: flex;
|
|
54
54
|
align-items: center;
|
|
55
|
-
margin-right:
|
|
55
|
+
margin-right: 12px;
|
|
56
56
|
line-height: 1;
|
|
57
57
|
img,
|
|
58
58
|
svg {
|
|
59
59
|
width: auto;
|
|
60
|
-
height:
|
|
61
|
-
max-height:
|
|
60
|
+
height: 40px;
|
|
61
|
+
max-height: 40px;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
.footer-brand-name {
|
|
65
|
-
font-size:
|
|
66
|
-
|
|
65
|
+
font-size: 18px;
|
|
66
|
+
color: ${(props) => props.theme.palette.grey[900]};
|
|
67
67
|
}
|
|
68
68
|
.footer-brand-desc {
|
|
69
|
+
white-space: pre-line;
|
|
69
70
|
margin-top: 16px;
|
|
71
|
+
color: #9397a1;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
${(props) => props.theme.breakpoints.down('sm')} {
|
package/src/Footer/index.jsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
+
import { useCreation } from 'ahooks';
|
|
3
4
|
import { styled } from '@arcblock/ux/lib/Theme';
|
|
4
5
|
import { withErrorBoundary } from 'react-error-boundary';
|
|
5
6
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
7
|
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
7
8
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
8
9
|
import omit from 'lodash/omit';
|
|
10
|
+
import isFinite from 'lodash/isFinite';
|
|
9
11
|
|
|
10
12
|
import OverridableThemeProvider from '../common/overridable-theme-provider';
|
|
11
13
|
import InternalFooter from './internal-footer';
|
|
@@ -13,6 +15,7 @@ import { mapRecursive } from '../utils';
|
|
|
13
15
|
import { formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
14
16
|
import { BlockletMetaProps } from '../types';
|
|
15
17
|
import withHideWhenEmbed from '../libs/with-hide-when-embed';
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* 专门用于 (composable) blocklet 的 Footer 组件, 基于 blocklet meta 中的数据渲染
|
|
18
21
|
*/
|
|
@@ -27,14 +30,37 @@ function Footer({ meta, theme: themeOverrides, ...rest }) {
|
|
|
27
30
|
return blocklet;
|
|
28
31
|
}
|
|
29
32
|
}, [meta]);
|
|
33
|
+
const productsNav = useCreation(() => {
|
|
34
|
+
return {
|
|
35
|
+
title: { en: 'Products', zh: '产品' },
|
|
36
|
+
section: ['footer'],
|
|
37
|
+
items: [
|
|
38
|
+
{ title: 'ArcSphere', link: `https://www.arcblock.io/content/tags/${locale}/arcsphere`, isNew: true },
|
|
39
|
+
{ title: 'DID Wallet', link: `https://www.didwallet.io/${locale}` },
|
|
40
|
+
{ title: 'DID Spaces', link: `https://www.didspaces.com/${locale}` },
|
|
41
|
+
{ title: 'DID Name Service', link: `https://www.didnames.io/${locale}` },
|
|
42
|
+
{ title: 'Blocklet Launcher', link: `https://launcher.arcblock.io/${locale}` },
|
|
43
|
+
{ title: 'Blocklet Server', link: `https://www.arcblock.io/content/collections/${locale}/blocklet-server` },
|
|
44
|
+
{ title: 'AIGNE', link: `https://www.aigne.io/${locale}` },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}, [locale]);
|
|
48
|
+
|
|
30
49
|
if (!formattedBlocklet.appName) {
|
|
31
50
|
return null;
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
const { appLogo, appLogoRect, appName, appDescription, description, theme, copyright } = formattedBlocklet;
|
|
54
|
+
const navFooter = [...(formattedBlocklet?.navigation?.footer ?? [])];
|
|
55
|
+
|
|
56
|
+
// 显示 Products 导航
|
|
57
|
+
const productsNavOrder = parseInt(window.blocklet?.USE_ARCBLOCK_THEME, 10);
|
|
58
|
+
if (isFinite(productsNavOrder)) {
|
|
59
|
+
navFooter.splice(productsNavOrder, 0, productsNav);
|
|
60
|
+
}
|
|
35
61
|
|
|
36
62
|
const localized = {
|
|
37
|
-
footerNav: getLocalizedNavigation(
|
|
63
|
+
footerNav: getLocalizedNavigation(navFooter, locale) || [],
|
|
38
64
|
socialMedia: getLocalizedNavigation(formattedBlocklet?.navigation?.social, locale) || [],
|
|
39
65
|
links: getLocalizedNavigation(formattedBlocklet?.navigation?.bottom, locale) || [],
|
|
40
66
|
};
|
|
@@ -35,7 +35,7 @@ function InternalFooter(props) {
|
|
|
35
35
|
return brand ? <Brand {...brand} /> : null;
|
|
36
36
|
};
|
|
37
37
|
const renderNavigation = () => {
|
|
38
|
-
return navigation?.length ? <Links links={navigation} /> : null;
|
|
38
|
+
return navigation?.length ? <Links links={navigation} columns={3} /> : null;
|
|
39
39
|
};
|
|
40
40
|
const renderSocialMedia = () => {
|
|
41
41
|
return socialMedia?.length ? <SocialMedia items={socialMedia} /> : null;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
+
import clsx from 'clsx';
|
|
2
3
|
import Box from '@mui/material/Box';
|
|
4
|
+
import { grey } from '@mui/material/colors';
|
|
3
5
|
import Container from '@mui/material/Container';
|
|
4
6
|
import { styled } from '@arcblock/ux/lib/Theme';
|
|
5
7
|
|
|
@@ -8,25 +10,52 @@ import Row from './row';
|
|
|
8
10
|
/**
|
|
9
11
|
* footer standard layout
|
|
10
12
|
*/
|
|
11
|
-
function StandardLayout({ elements, data, ...rest }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
function StandardLayout({ elements, data, className, ...rest }) {
|
|
14
|
+
const withNavigation = !!data.navigation?.length;
|
|
15
|
+
let topSection = null;
|
|
16
|
+
|
|
17
|
+
if (withNavigation) {
|
|
18
|
+
// 左 brand & social,右导航栏
|
|
19
|
+
topSection = (
|
|
20
|
+
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, justifyContent: 'space-between' }}>
|
|
15
21
|
<Box
|
|
16
22
|
sx={{
|
|
23
|
+
flex: '1 1 auto',
|
|
24
|
+
paddingRight: { xs: 0, md: 3 },
|
|
17
25
|
display: 'flex',
|
|
18
|
-
flexDirection:
|
|
19
|
-
|
|
20
|
-
alignItems: { xs: 'center', md: 'space-between' },
|
|
26
|
+
flexDirection: 'column',
|
|
27
|
+
alignItems: { xs: 'center', md: 'flex-start' },
|
|
21
28
|
gap: 2,
|
|
22
29
|
pb: 3,
|
|
23
30
|
}}>
|
|
24
31
|
<Box>{elements.brand}</Box>
|
|
25
32
|
<Box lineHeight={1}>{elements.socialMedia}</Box>
|
|
26
33
|
</Box>
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
<Box sx={{ mb: 3, borderTop: { xs: `1px solid ${grey[200]}`, md: 0 } }}>{elements.navigation}</Box>
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
// 左 brand,右 social
|
|
39
|
+
topSection = (
|
|
40
|
+
<Box
|
|
41
|
+
sx={{
|
|
42
|
+
display: 'flex',
|
|
43
|
+
flexDirection: { xs: 'column', md: 'row' },
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
alignItems: { xs: 'center', md: 'space-between' },
|
|
46
|
+
gap: 2,
|
|
47
|
+
pb: 3,
|
|
48
|
+
}}>
|
|
49
|
+
<Box>{elements.brand}</Box>
|
|
50
|
+
<Box lineHeight={1}>{elements.socialMedia}</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Root {...rest} className={clsx({ 'footer--with-navs': withNavigation }, className)}>
|
|
57
|
+
<Container>
|
|
58
|
+
{topSection}
|
|
30
59
|
<Row sx={{ pt: 3, borderTop: 1, borderColor: 'grey.200' }} autoCenter>
|
|
31
60
|
{elements.copyright}
|
|
32
61
|
{elements.links}
|
|
@@ -45,6 +74,7 @@ StandardLayout.propTypes = {
|
|
|
45
74
|
links: PropTypes.element,
|
|
46
75
|
}).isRequired,
|
|
47
76
|
data: PropTypes.object.isRequired,
|
|
77
|
+
className: PropTypes.string,
|
|
48
78
|
};
|
|
49
79
|
|
|
50
80
|
const Root = styled('div')`
|
|
@@ -53,6 +83,14 @@ const Root = styled('div')`
|
|
|
53
83
|
.footer-brand-desc {
|
|
54
84
|
display: none;
|
|
55
85
|
}
|
|
86
|
+
&.footer--with-navs {
|
|
87
|
+
${(props) => props.theme.breakpoints.up('md')} {
|
|
88
|
+
.footer-brand-desc {
|
|
89
|
+
max-width: 360px;
|
|
90
|
+
display: block;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
56
94
|
&& .footer-brand-logo {
|
|
57
95
|
margin-right: 0;
|
|
58
96
|
}
|
package/src/Footer/links.jsx
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/* eslint-disable react/no-array-index-key */
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
+
import { useCreation } from 'ahooks';
|
|
5
|
+
import isInteger from 'lodash/isInteger';
|
|
4
6
|
import { styled } from '@arcblock/ux/lib/Theme';
|
|
5
7
|
import clsx from 'clsx';
|
|
6
8
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
7
9
|
import Icon from '../Icon';
|
|
10
|
+
import useMobile from '../hooks/use-mobile';
|
|
11
|
+
import { splitNavColumns } from '../utils';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* footer 中的 links (支持分组, 最多支持 2 级)
|
|
11
15
|
* TODO: dark/light theme
|
|
12
16
|
*/
|
|
13
|
-
export default function Links({ links, flowLayout, ...rest }) {
|
|
17
|
+
export default function Links({ links, flowLayout, columns, ...rest }) {
|
|
14
18
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
15
|
-
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
19
|
+
const isMobile = useMobile({ key: 'md' });
|
|
18
20
|
// 只要发现一项元素有子元素, 就认为是分组 (大字号突出 group title)
|
|
19
21
|
const isGroupMode = links.some((item) => item.items?.length);
|
|
22
|
+
// 是否启用 columns 布局
|
|
23
|
+
const columnsLayout = !isMobile && isGroupMode && isInteger(columns) && columns > 1;
|
|
20
24
|
const renderItem = ({ label, link, icon, render, props }) => {
|
|
21
25
|
let result = label;
|
|
22
26
|
if (render) {
|
|
@@ -35,56 +39,101 @@ export default function Links({ links, flowLayout, ...rest }) {
|
|
|
35
39
|
</>
|
|
36
40
|
);
|
|
37
41
|
};
|
|
42
|
+
const content = useCreation(() => {
|
|
43
|
+
if (!links?.length) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// 流布局
|
|
47
|
+
if (flowLayout) {
|
|
48
|
+
return links.map((item, i) => (
|
|
49
|
+
<span key={i} className="footer-links-item">
|
|
50
|
+
{renderItem(item)}
|
|
51
|
+
</span>
|
|
52
|
+
));
|
|
53
|
+
}
|
|
54
|
+
// 列布局
|
|
55
|
+
if (columnsLayout) {
|
|
56
|
+
return splitNavColumns(links, { columns }).map((cols, i) => {
|
|
57
|
+
return (
|
|
58
|
+
<div key={i} className="footer-links-column">
|
|
59
|
+
{cols
|
|
60
|
+
.filter((v) => v.group)
|
|
61
|
+
.map((item, j) => {
|
|
62
|
+
const { items } = item;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div key={j} className="footer-links-group">
|
|
66
|
+
<span className="footer-links-item">{renderItem(item)}</span>
|
|
67
|
+
{!!items?.length && (
|
|
68
|
+
<div className="footer-links-sub">
|
|
69
|
+
{items.map((child, k) => (
|
|
70
|
+
<span
|
|
71
|
+
key={k}
|
|
72
|
+
className={clsx('footer-links-item', { 'footer-links-item--new': child.isNew })}>
|
|
73
|
+
{renderItem(child)}
|
|
74
|
+
</span>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// 纯 flex 布局
|
|
86
|
+
return links.map((item, i) => {
|
|
87
|
+
const { items } = item;
|
|
88
|
+
// 用于移动端展开
|
|
89
|
+
const isActive = i === activeIndex;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
key={i}
|
|
94
|
+
className={clsx('footer-links-group', {
|
|
95
|
+
'footer-links-group--active': isActive,
|
|
96
|
+
})}
|
|
97
|
+
onClick={() => setActiveIndex(activeIndex === i ? -1 : i)}>
|
|
98
|
+
<span className="footer-links-item">
|
|
99
|
+
{renderItem(item)}
|
|
100
|
+
{!!items?.length && (
|
|
101
|
+
<span className="footer-links-group-expand-icon">
|
|
102
|
+
<ExpandMoreIcon
|
|
103
|
+
style={{
|
|
104
|
+
transform: `rotate(${isActive ? 180 : 0}deg)`,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</span>
|
|
110
|
+
{!!items?.length && (
|
|
111
|
+
<div className="footer-links-sub">
|
|
112
|
+
{items.map((child, j) => (
|
|
113
|
+
<span key={j} className={clsx('footer-links-item', { 'footer-links-item--new': child.isNew })}>
|
|
114
|
+
{renderItem(child)}
|
|
115
|
+
</span>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
}, [links, flowLayout, columnsLayout, activeIndex]);
|
|
123
|
+
|
|
124
|
+
if (!links?.length) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
38
128
|
return (
|
|
39
129
|
<Root
|
|
40
130
|
{...rest}
|
|
41
131
|
className={clsx(rest.className, {
|
|
42
132
|
'footer-links--grouped': isGroupMode,
|
|
43
133
|
'footer-links--flow': flowLayout,
|
|
134
|
+
'footer-links--columns': columnsLayout,
|
|
44
135
|
})}>
|
|
45
|
-
<div className="footer-links-inner">
|
|
46
|
-
{flowLayout &&
|
|
47
|
-
links.map((item, i) => (
|
|
48
|
-
<span key={i} className="footer-links-item">
|
|
49
|
-
{renderItem(item)}
|
|
50
|
-
</span>
|
|
51
|
-
))}
|
|
52
|
-
{!flowLayout &&
|
|
53
|
-
links.map((item, i) => {
|
|
54
|
-
const { items } = item;
|
|
55
|
-
const isActive = i === activeIndex;
|
|
56
|
-
return (
|
|
57
|
-
<div
|
|
58
|
-
key={i}
|
|
59
|
-
className={clsx('footer-links-group', {
|
|
60
|
-
'footer-links-group--active': isActive,
|
|
61
|
-
})}
|
|
62
|
-
onClick={() => setActiveIndex(activeIndex === i ? -1 : i)}>
|
|
63
|
-
<span className="footer-links-item">
|
|
64
|
-
{renderItem(item)}
|
|
65
|
-
{!!items?.length && (
|
|
66
|
-
<span className="footer-links-group-expand-icon">
|
|
67
|
-
<ExpandMoreIcon
|
|
68
|
-
style={{
|
|
69
|
-
transform: `rotate(${isActive ? 180 : 0}deg)`,
|
|
70
|
-
}}
|
|
71
|
-
/>
|
|
72
|
-
</span>
|
|
73
|
-
)}
|
|
74
|
-
</span>
|
|
75
|
-
{!!items?.length && (
|
|
76
|
-
<div className="footer-links-sub">
|
|
77
|
-
{items.map((child, j) => (
|
|
78
|
-
<span key={j} className="footer-links-item">
|
|
79
|
-
{renderItem(child)}
|
|
80
|
-
</span>
|
|
81
|
-
))}
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</div>
|
|
136
|
+
<div className="footer-links-inner">{content}</div>
|
|
88
137
|
</Root>
|
|
89
138
|
);
|
|
90
139
|
}
|
|
@@ -100,6 +149,8 @@ Links.propTypes = {
|
|
|
100
149
|
),
|
|
101
150
|
// 流动布局, 简单的从左到右排列
|
|
102
151
|
flowLayout: PropTypes.bool,
|
|
152
|
+
// 列布局
|
|
153
|
+
columns: PropTypes.number,
|
|
103
154
|
};
|
|
104
155
|
|
|
105
156
|
Links.defaultProps = {
|
|
@@ -109,7 +160,7 @@ Links.defaultProps = {
|
|
|
109
160
|
|
|
110
161
|
const Root = styled('div')`
|
|
111
162
|
overflow: hidden;
|
|
112
|
-
color:
|
|
163
|
+
color: #9397a1;
|
|
113
164
|
.footer-links-inner {
|
|
114
165
|
display: flex;
|
|
115
166
|
justify-content: space-between;
|
|
@@ -120,9 +171,6 @@ const Root = styled('div')`
|
|
|
120
171
|
display: flex;
|
|
121
172
|
flex-direction: column;
|
|
122
173
|
}
|
|
123
|
-
.footer-links-sub .footer-links-item {
|
|
124
|
-
color: ${(props) => props.theme.palette.grey[900]};
|
|
125
|
-
}
|
|
126
174
|
.footer-links-group-expand-icon {
|
|
127
175
|
display: none;
|
|
128
176
|
position: absolute;
|
|
@@ -139,13 +187,22 @@ const Root = styled('div')`
|
|
|
139
187
|
display: inline-flex;
|
|
140
188
|
align-items: center;
|
|
141
189
|
position: relative;
|
|
142
|
-
padding:
|
|
190
|
+
padding: 6px 8px;
|
|
143
191
|
font-size: 14px;
|
|
192
|
+
&--new::after {
|
|
193
|
+
content: 'New';
|
|
194
|
+
color: #4672ea;
|
|
195
|
+
background-color: #e1e8fb;
|
|
196
|
+
padding: 1px 8px;
|
|
197
|
+
border-radius: 10px/50%;
|
|
198
|
+
margin-left: 8px;
|
|
199
|
+
}
|
|
144
200
|
}
|
|
145
201
|
&.footer-links--grouped {
|
|
146
202
|
.footer-links-group {
|
|
147
203
|
> .footer-links-item {
|
|
148
|
-
font-weight:
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
color: #25292f;
|
|
149
206
|
}
|
|
150
207
|
.footer-links-sub {
|
|
151
208
|
margin-top: 8px;
|
|
@@ -157,11 +214,29 @@ const Root = styled('div')`
|
|
|
157
214
|
max-width: 150px;
|
|
158
215
|
color: inherit;
|
|
159
216
|
text-decoration: none;
|
|
217
|
+
transition: color 0.2s ease-in-out;
|
|
160
218
|
&:hover {
|
|
161
|
-
|
|
219
|
+
color: #25292f;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/* columns 布局 */
|
|
223
|
+
&.footer-links--columns {
|
|
224
|
+
.footer-links-inner {
|
|
225
|
+
gap: 96px;
|
|
226
|
+
}
|
|
227
|
+
.footer-links-column {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
}
|
|
231
|
+
.footer-links-group {
|
|
232
|
+
.footer-links-sub {
|
|
233
|
+
margin-top: 2px;
|
|
234
|
+
margin-bottom: 12px;
|
|
235
|
+
}
|
|
162
236
|
}
|
|
163
237
|
}
|
|
164
238
|
|
|
239
|
+
/* flow 布局 */
|
|
165
240
|
&.footer-links--flow {
|
|
166
241
|
display: inline-flex;
|
|
167
242
|
.footer-links-inner {
|
|
@@ -183,6 +258,7 @@ const Root = styled('div')`
|
|
|
183
258
|
}
|
|
184
259
|
}
|
|
185
260
|
|
|
261
|
+
/* 移动端样式 */
|
|
186
262
|
${(props) => props.theme.breakpoints.down('md')} {
|
|
187
263
|
.footer-links-inner {
|
|
188
264
|
flex-direction: column;
|
|
@@ -54,8 +54,9 @@ const Root = styled('div')`
|
|
|
54
54
|
a {
|
|
55
55
|
color: ${(props) => props.theme.palette.grey[400]};
|
|
56
56
|
text-decoration: none;
|
|
57
|
+
transition: color 0.2s ease-in-out;
|
|
57
58
|
&:hover {
|
|
58
|
-
color:
|
|
59
|
+
color: #25292f;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
${(props) => props.theme.breakpoints.down('md')} {
|
package/src/Header/index.tsx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { useMemoizedFn } from 'ahooks';
|
|
2
3
|
import { withErrorBoundary } from 'react-error-boundary';
|
|
3
4
|
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
4
5
|
import { styled } from '@arcblock/ux/lib/Theme';
|
|
5
6
|
import { ResponsiveHeader } from '@arcblock/ux/lib/Header';
|
|
6
|
-
import NavMenu from '@arcblock/ux/lib/NavMenu';
|
|
7
|
+
import NavMenu, { Products } from '@arcblock/ux/lib/NavMenu';
|
|
7
8
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
9
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
10
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
9
11
|
import omit from 'lodash/omit';
|
|
12
|
+
import isFinite from 'lodash/isFinite';
|
|
10
13
|
import type { BoxProps, Breakpoint } from '@mui/material';
|
|
11
14
|
import clsx from 'clsx';
|
|
12
15
|
|
|
@@ -20,6 +23,15 @@ import { BlockletMetaProps, SessionManagerProps } from '../@types';
|
|
|
20
23
|
import withHideWhenEmbed from '../libs/with-hide-when-embed';
|
|
21
24
|
import useMobile from '../hooks/use-mobile';
|
|
22
25
|
|
|
26
|
+
const translations = {
|
|
27
|
+
en: {
|
|
28
|
+
products: 'Products',
|
|
29
|
+
},
|
|
30
|
+
zh: {
|
|
31
|
+
products: '产品',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
// blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
|
|
24
36
|
const parseNavigation = (navigation: any) => {
|
|
25
37
|
if (!navigation?.length) {
|
|
@@ -98,6 +110,9 @@ function Header({
|
|
|
98
110
|
}: HeaderProps & Omit<BoxProps, keyof HeaderProps>) {
|
|
99
111
|
useWalletHiddenTopbar();
|
|
100
112
|
const { locale } = useLocaleContext() || {};
|
|
113
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
114
|
+
return translate(translations, key, locale, 'en', data);
|
|
115
|
+
});
|
|
101
116
|
const formattedBlocklet = useMemo(() => {
|
|
102
117
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
103
118
|
try {
|
|
@@ -117,6 +132,16 @@ function Header({
|
|
|
117
132
|
const parsedNavigation = parseNavigation(navigation);
|
|
118
133
|
const { navItems, activeId } = parsedNavigation;
|
|
119
134
|
|
|
135
|
+
// 显示 Products 导航
|
|
136
|
+
const productsNavOrder = parseInt(window.blocklet?.USE_ARCBLOCK_THEME, 10);
|
|
137
|
+
if (isFinite(productsNavOrder)) {
|
|
138
|
+
navItems.splice(productsNavOrder, 0, {
|
|
139
|
+
label: t('products'),
|
|
140
|
+
// eslint-disable-next-line react/no-unstable-nested-components
|
|
141
|
+
children: ({ isOpen }: { isOpen: boolean }) => <Products isOpen={isOpen} />,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
121
146
|
const _addons =
|
|
122
147
|
typeof addons === 'function'
|
package/src/utils.js
CHANGED
|
@@ -80,3 +80,74 @@ export const matchPaths = (paths = []) => {
|
|
|
80
80
|
}, matched[0]);
|
|
81
81
|
return mostSpecific.index;
|
|
82
82
|
};
|
|
83
|
+
|
|
84
|
+
/** 导航列表分列 */
|
|
85
|
+
export const splitNavColumns = (items, options = {}) => {
|
|
86
|
+
const { columns = 1, breakInside = false, groupHeight = 48, itemHeight = 24, childrenKey = 'items' } = options;
|
|
87
|
+
|
|
88
|
+
// 高度预估
|
|
89
|
+
const totalHeight = items.reduce((height, group) => {
|
|
90
|
+
return height + groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
|
|
91
|
+
}, 0);
|
|
92
|
+
const targetHeight = Math.ceil(totalHeight / columns);
|
|
93
|
+
|
|
94
|
+
// 使用贪心策略进行分列
|
|
95
|
+
const result = [[]];
|
|
96
|
+
let currentColumn = 0;
|
|
97
|
+
let currentHeight = 0;
|
|
98
|
+
|
|
99
|
+
// 允许的高度偏差范围(有利于得到高度相差不大的列)
|
|
100
|
+
const heightVariance = targetHeight * 0.2;
|
|
101
|
+
|
|
102
|
+
// 是否应该分列
|
|
103
|
+
const shouldBreakColumn = (nextHeight) => {
|
|
104
|
+
return (
|
|
105
|
+
currentHeight > targetHeight - heightVariance &&
|
|
106
|
+
currentColumn < columns - 1 &&
|
|
107
|
+
currentHeight + nextHeight > targetHeight + heightVariance
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
items.forEach((group) => {
|
|
112
|
+
const groupTotalHeight = groupHeight + (group[childrenKey]?.length || 0) * itemHeight;
|
|
113
|
+
|
|
114
|
+
// 允许截断分组时,可以在任何子项处换列
|
|
115
|
+
if (breakInside && shouldBreakColumn(groupHeight)) {
|
|
116
|
+
currentColumn++;
|
|
117
|
+
currentHeight = 0;
|
|
118
|
+
result[currentColumn] = [];
|
|
119
|
+
}
|
|
120
|
+
// 不允许截断分组时,只能在分组边界换列
|
|
121
|
+
if (!breakInside && currentHeight > 0 && shouldBreakColumn(groupTotalHeight)) {
|
|
122
|
+
currentColumn++;
|
|
123
|
+
currentHeight = 0;
|
|
124
|
+
result[currentColumn] = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 添加分组标题
|
|
128
|
+
result[currentColumn].push({
|
|
129
|
+
...group,
|
|
130
|
+
group: true,
|
|
131
|
+
});
|
|
132
|
+
currentHeight += groupHeight;
|
|
133
|
+
|
|
134
|
+
// 添加子项
|
|
135
|
+
if (group[childrenKey]) {
|
|
136
|
+
group[childrenKey].forEach((child) => {
|
|
137
|
+
if (breakInside && shouldBreakColumn(itemHeight)) {
|
|
138
|
+
currentColumn++;
|
|
139
|
+
currentHeight = 0;
|
|
140
|
+
result[currentColumn] = [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
result[currentColumn].push({
|
|
144
|
+
...child,
|
|
145
|
+
group: false,
|
|
146
|
+
});
|
|
147
|
+
currentHeight += itemHeight;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
};
|