@arcblock/ux 2.12.3 → 2.12.4
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/NavMenu/nav-menu.d.ts +1 -1
- package/lib/NavMenu/nav-menu.js +22 -12
- package/lib/NavMenu/products.js +38 -27
- package/lib/NavMenu/style.d.ts +2 -2
- package/lib/NavMenu/style.js +46 -25
- package/lib/NavMenu/sub-container.d.ts +6 -0
- package/lib/NavMenu/sub-container.js +83 -0
- package/package.json +5 -5
- package/src/NavMenu/nav-menu.tsx +26 -15
- package/src/NavMenu/products.tsx +104 -42
- package/src/NavMenu/style.ts +40 -22
- package/src/NavMenu/sub-container.tsx +96 -0
@@ -38,7 +38,7 @@ export interface ItemProps extends React.HTMLAttributes<HTMLLIElement> {
|
|
38
38
|
}
|
39
39
|
export declare const Item: import("react").ForwardRefExoticComponent<ItemProps & import("react").RefAttributes<HTMLLIElement>>;
|
40
40
|
export interface SubProps extends Omit<ItemProps, 'children' | 'active'> {
|
41
|
-
children?: Array<React.ReactElement> | ((props: {
|
41
|
+
children?: Array<React.ReactElement | null> | ((props: {
|
42
42
|
isOpen: boolean;
|
43
43
|
}) => React.ReactElement | null);
|
44
44
|
expandIcon?: React.ReactNode | ((props: {
|
package/lib/NavMenu/nav-menu.js
CHANGED
@@ -5,7 +5,8 @@ import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuI
|
|
5
5
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
6
6
|
import { useCreation, useMemoizedFn, useReactive, useSize, useThrottleFn } from 'ahooks';
|
7
7
|
import { NavMenuProvider, useNavMenuContext } from './nav-menu-context';
|
8
|
-
import { NavMenuRoot,
|
8
|
+
import { NavMenuRoot, NavMenuItem, NavMenuSub, NavMenuSubList, NavMenuStyled } from './style';
|
9
|
+
import { SubContainer } from './sub-container';
|
9
10
|
|
10
11
|
// 过滤 children 中的 Item/Sub, 忽略其它类型的 element
|
11
12
|
function filterItems(children) {
|
@@ -73,6 +74,7 @@ function NavMenu({
|
|
73
74
|
const navMenuRef = useRef(null);
|
74
75
|
const itemRefs = useRef([]);
|
75
76
|
const moreIconRef = useRef(null);
|
77
|
+
const containerWidth = useRef(0);
|
76
78
|
const isAllItemsHidden = currentState.hiddenItemCount === itemRefs.current?.length;
|
77
79
|
const style = isAllItemsHidden ? {
|
78
80
|
marginLeft: '0px'
|
@@ -96,14 +98,13 @@ function NavMenu({
|
|
96
98
|
let totalWidthUsed = 0;
|
97
99
|
let newHiddenCount = 0;
|
98
100
|
let leftAllHidden = false;
|
99
|
-
const containerWidth = containerSize?.width || 0;
|
100
101
|
const moreIconWidth = moreIconRef.current ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft) : 0;
|
101
102
|
itemRefs.current.forEach((item, index) => {
|
102
103
|
if (item) {
|
103
104
|
item.style.display = 'flex';
|
104
105
|
const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
|
105
106
|
const currentItemWidth = item.offsetWidth + marginLeft;
|
106
|
-
if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
|
107
|
+
if (containerWidth.current - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
|
107
108
|
totalWidthUsed += currentItemWidth;
|
108
109
|
} else {
|
109
110
|
item.style.display = 'none';
|
@@ -119,6 +120,7 @@ function NavMenu({
|
|
119
120
|
wait: 100
|
120
121
|
});
|
121
122
|
useLayoutEffect(() => {
|
123
|
+
containerWidth.current = containerSize?.width || navMenuRef.current?.clientWidth || 0;
|
122
124
|
if (mode === 'horizontal') {
|
123
125
|
checkItemsFit();
|
124
126
|
}
|
@@ -144,8 +146,14 @@ function NavMenu({
|
|
144
146
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
145
147
|
}, [activeId]);
|
146
148
|
const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
|
147
|
-
const renderItem = (item, index,
|
149
|
+
const renderItem = (item, index, level = 0) => {
|
150
|
+
const isTopLevel = level === 0;
|
148
151
|
if (item?.children) {
|
152
|
+
// 只渲染两级子菜单
|
153
|
+
if (level > 0) {
|
154
|
+
return null;
|
155
|
+
}
|
156
|
+
|
149
157
|
// 对于 Sub 组件,如果它是顶级组件,则包含 ref
|
150
158
|
return /*#__PURE__*/_jsx(Sub, {
|
151
159
|
id: item.id,
|
@@ -158,7 +166,7 @@ function NavMenu({
|
|
158
166
|
children: typeof item.children === 'function' ? item.children : item.children.map((childItem, childIndex) => renderItem({
|
159
167
|
...childItem,
|
160
168
|
variant: 'panel'
|
161
|
-
}, childIndex,
|
169
|
+
}, childIndex, level + 1))
|
162
170
|
}, item.id);
|
163
171
|
}
|
164
172
|
|
@@ -175,18 +183,21 @@ function NavMenu({
|
|
175
183
|
} : undefined
|
176
184
|
}, item.id);
|
177
185
|
};
|
178
|
-
const content = items ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index)) : children?.slice(-currentState.hiddenItemCount);
|
186
|
+
const content = items ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index, 1)) : children?.slice(-currentState.hiddenItemCount);
|
187
|
+
|
188
|
+
// 当前展开的子菜单
|
189
|
+
const openedId = currentState.openedIds[0];
|
179
190
|
return /*#__PURE__*/_jsx(NavMenuProvider, {
|
180
191
|
value: contextValue,
|
181
|
-
children: /*#__PURE__*/_jsx(
|
192
|
+
children: /*#__PURE__*/_jsx(NavMenuStyled, {
|
182
193
|
...rest,
|
183
194
|
className: classes,
|
184
195
|
$textColor: textColor,
|
185
196
|
$bgColor: bgColor,
|
186
|
-
children: /*#__PURE__*/_jsxs(
|
187
|
-
className: clsx('navmenu-
|
197
|
+
children: /*#__PURE__*/_jsxs(NavMenuRoot, {
|
198
|
+
className: clsx('navmenu-root', `navmenu-root--${mode}`),
|
188
199
|
ref: navMenuRef,
|
189
|
-
children: [items ? items.map((item, index) => renderItem(item, index
|
200
|
+
children: [items ? items.map((item, index) => renderItem(item, index)) : renderChildrenWithRef(children || []), currentState.hiddenItemCount > 0 && /*#__PURE__*/_jsx(Sub, {
|
190
201
|
expandIcon: false,
|
191
202
|
icon: icon,
|
192
203
|
label: "",
|
@@ -329,8 +340,7 @@ export const Sub = /*#__PURE__*/forwardRef(({
|
|
329
340
|
children: typeof expandIcon === 'function' ? expandIcon({
|
330
341
|
isOpen
|
331
342
|
}) : expandIcon
|
332
|
-
}), /*#__PURE__*/_jsx(
|
333
|
-
className: "navmenu-sub__container",
|
343
|
+
}), /*#__PURE__*/_jsx(SubContainer, {
|
334
344
|
...containerProps,
|
335
345
|
children: typeof children === 'function' ? children({
|
336
346
|
isOpen
|
package/lib/NavMenu/products.js
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
2
|
import React from "react";
|
3
|
-
import {
|
3
|
+
import { useRef } from 'react';
|
4
4
|
import { Link } from 'react-router-dom';
|
5
5
|
import { useCreation, useMemoizedFn } from 'ahooks';
|
6
6
|
import { Box, Grid } from '@mui/material';
|
7
|
-
import { useWindowSize } from 'react-use';
|
8
7
|
import SubItemGroup from './sub-item-group';
|
9
8
|
import { Item } from './nav-menu';
|
10
9
|
import { styled } from '../Theme';
|
@@ -1437,10 +1436,6 @@ export default function Products({
|
|
1437
1436
|
mode
|
1438
1437
|
} = useNavMenuContext();
|
1439
1438
|
const wrapperRef = useRef(null);
|
1440
|
-
const {
|
1441
|
-
width,
|
1442
|
-
height
|
1443
|
-
} = useWindowSize();
|
1444
1439
|
const {
|
1445
1440
|
locale = 'en'
|
1446
1441
|
} = useLocaleContext() || {};
|
@@ -1452,6 +1447,8 @@ export default function Products({
|
|
1452
1447
|
children: [[{
|
1453
1448
|
label: /*#__PURE__*/_jsx(Link, {
|
1454
1449
|
to: `https://www.nftstudio.rocks/${locale}`,
|
1450
|
+
target: "_blank",
|
1451
|
+
rel: "noreferrer noopener",
|
1455
1452
|
children: "NFT Studio"
|
1456
1453
|
}),
|
1457
1454
|
description: t('products.nftStudio.description'),
|
@@ -1459,6 +1456,8 @@ export default function Products({
|
|
1459
1456
|
}, {
|
1460
1457
|
label: /*#__PURE__*/_jsx(Link, {
|
1461
1458
|
to: `https://www.arcblock.io/content/collections/${locale}/creator-studio`,
|
1459
|
+
target: "_blank",
|
1460
|
+
rel: "noreferrer noopener",
|
1462
1461
|
children: "Creator Studio"
|
1463
1462
|
}),
|
1464
1463
|
description: t('products.creatorStudio.description'),
|
@@ -1466,6 +1465,8 @@ export default function Products({
|
|
1466
1465
|
}], [{
|
1467
1466
|
label: /*#__PURE__*/_jsx(Link, {
|
1468
1467
|
to: `https://www.aigne.io/${locale}`,
|
1468
|
+
target: "_blank",
|
1469
|
+
rel: "noreferrer noopener",
|
1469
1470
|
children: "AIGNE"
|
1470
1471
|
}),
|
1471
1472
|
description: t('products.aigne.description'),
|
@@ -1473,6 +1474,8 @@ export default function Products({
|
|
1473
1474
|
}, {
|
1474
1475
|
label: /*#__PURE__*/_jsx(Link, {
|
1475
1476
|
to: `https://www.aistro.io/${locale}`,
|
1477
|
+
target: "_blank",
|
1478
|
+
rel: "noreferrer noopener",
|
1476
1479
|
children: "Aistro"
|
1477
1480
|
}),
|
1478
1481
|
description: t('products.aistro.description'),
|
@@ -1484,6 +1487,8 @@ export default function Products({
|
|
1484
1487
|
children: [[{
|
1485
1488
|
label: /*#__PURE__*/_jsx(Link, {
|
1486
1489
|
to: `https://launcher.arcblock.io/${locale}`,
|
1490
|
+
target: "_blank",
|
1491
|
+
rel: "noreferrer noopener",
|
1487
1492
|
children: "Blocklet Launcher"
|
1488
1493
|
}),
|
1489
1494
|
description: t('products.blockletLauncher.description'),
|
@@ -1491,6 +1496,8 @@ export default function Products({
|
|
1491
1496
|
}, {
|
1492
1497
|
label: /*#__PURE__*/_jsx(Link, {
|
1493
1498
|
to: `https://www.arcblock.io/content/collections/${locale}/ai-kit`,
|
1499
|
+
target: "_blank",
|
1500
|
+
rel: "noreferrer noopener",
|
1494
1501
|
children: "Al Kit"
|
1495
1502
|
}),
|
1496
1503
|
description: t('products.alKit.description'),
|
@@ -1498,6 +1505,8 @@ export default function Products({
|
|
1498
1505
|
}], [{
|
1499
1506
|
label: /*#__PURE__*/_jsx(Link, {
|
1500
1507
|
to: `https://store.blocklet.dev/${locale}`,
|
1508
|
+
target: "_blank",
|
1509
|
+
rel: "noreferrer noopener",
|
1501
1510
|
children: "Blocklet Store"
|
1502
1511
|
}),
|
1503
1512
|
description: t('products.blockletStore.description'),
|
@@ -1505,6 +1514,8 @@ export default function Products({
|
|
1505
1514
|
}, {
|
1506
1515
|
label: /*#__PURE__*/_jsx(Link, {
|
1507
1516
|
to: `https://www.web3kit.rocks/${locale}`,
|
1517
|
+
target: "_blank",
|
1518
|
+
rel: "noreferrer noopener",
|
1508
1519
|
children: "Web3 Kit"
|
1509
1520
|
}),
|
1510
1521
|
description: t('products.web3Kit.description'),
|
@@ -1516,6 +1527,8 @@ export default function Products({
|
|
1516
1527
|
children: [[{
|
1517
1528
|
label: /*#__PURE__*/_jsx(Link, {
|
1518
1529
|
to: `https://www.arcblock.io/content/collections/${locale}/blocklet`,
|
1530
|
+
target: "_blank",
|
1531
|
+
rel: "noreferrer noopener",
|
1519
1532
|
children: "Blocklet Framework"
|
1520
1533
|
}),
|
1521
1534
|
description: t('products.blockletFramework.description'),
|
@@ -1523,6 +1536,8 @@ export default function Products({
|
|
1523
1536
|
}, {
|
1524
1537
|
label: /*#__PURE__*/_jsx(Link, {
|
1525
1538
|
to: `https://www.didspaces.com/${locale}`,
|
1539
|
+
target: "_blank",
|
1540
|
+
rel: "noreferrer noopener",
|
1526
1541
|
children: "DID Spaces"
|
1527
1542
|
}),
|
1528
1543
|
description: t('products.didSpaces.description'),
|
@@ -1530,6 +1545,8 @@ export default function Products({
|
|
1530
1545
|
}, {
|
1531
1546
|
label: /*#__PURE__*/_jsx(Link, {
|
1532
1547
|
to: `https://main.abtnetwork.io/${locale}`,
|
1548
|
+
target: "_blank",
|
1549
|
+
rel: "noreferrer noopener",
|
1533
1550
|
children: "ABT Network"
|
1534
1551
|
}),
|
1535
1552
|
description: t('products.abtNetwork.description'),
|
@@ -1537,6 +1554,8 @@ export default function Products({
|
|
1537
1554
|
}], [{
|
1538
1555
|
label: /*#__PURE__*/_jsx(Link, {
|
1539
1556
|
to: `https://www.arcblock.io/content/collections/${locale}/blocklet-server`,
|
1557
|
+
target: "_blank",
|
1558
|
+
rel: "noreferrer noopener",
|
1540
1559
|
children: "Blocklet Server"
|
1541
1560
|
}),
|
1542
1561
|
description: t('products.blockletServer.description'),
|
@@ -1544,6 +1563,8 @@ export default function Products({
|
|
1544
1563
|
}, {
|
1545
1564
|
label: /*#__PURE__*/_jsx(Link, {
|
1546
1565
|
to: `https://www.arcblock.io/content/collections/${locale}/blockchain`,
|
1566
|
+
target: "_blank",
|
1567
|
+
rel: "noreferrer noopener",
|
1547
1568
|
children: "\u041E\u0421\u0410\u0420"
|
1548
1569
|
}),
|
1549
1570
|
description: t('products.osar.description'),
|
@@ -1555,6 +1576,8 @@ export default function Products({
|
|
1555
1576
|
children: [[{
|
1556
1577
|
label: /*#__PURE__*/_jsx(Link, {
|
1557
1578
|
to: `https://www.arcblock.io/content/collections/${locale}/did`,
|
1579
|
+
target: "_blank",
|
1580
|
+
rel: "noreferrer noopener",
|
1558
1581
|
children: "DID"
|
1559
1582
|
}),
|
1560
1583
|
description: t('products.did.description'),
|
@@ -1562,6 +1585,8 @@ export default function Products({
|
|
1562
1585
|
}, {
|
1563
1586
|
label: /*#__PURE__*/_jsx(Link, {
|
1564
1587
|
to: `https://www.didwallet.io/${locale}`,
|
1588
|
+
target: "_blank",
|
1589
|
+
rel: "noreferrer noopener",
|
1565
1590
|
children: "DID Wallet"
|
1566
1591
|
}),
|
1567
1592
|
description: t('products.didWallet.description'),
|
@@ -1569,13 +1594,17 @@ export default function Products({
|
|
1569
1594
|
}, {
|
1570
1595
|
label: /*#__PURE__*/_jsx(Link, {
|
1571
1596
|
to: `https://www.didnames.io/${locale}`,
|
1572
|
-
|
1597
|
+
target: "_blank",
|
1598
|
+
rel: "noreferrer noopener",
|
1599
|
+
children: "DID Names"
|
1573
1600
|
}),
|
1574
1601
|
description: t('products.didNameService.description'),
|
1575
1602
|
icon: /*#__PURE__*/_jsx(DidNameServiceSvg, {})
|
1576
1603
|
}], [{
|
1577
1604
|
label: /*#__PURE__*/_jsx(Link, {
|
1578
1605
|
to: `https://www.arcblock.io/content/collections/${locale}/verifiable-credential`,
|
1606
|
+
target: "_blank",
|
1607
|
+
rel: "noreferrer noopener",
|
1579
1608
|
children: "VC"
|
1580
1609
|
}),
|
1581
1610
|
description: t('products.vc.description'),
|
@@ -1583,6 +1612,8 @@ export default function Products({
|
|
1583
1612
|
}, {
|
1584
1613
|
label: /*#__PURE__*/_jsx(Link, {
|
1585
1614
|
to: `https://www.didconnect.io/${locale}`,
|
1615
|
+
target: "_blank",
|
1616
|
+
rel: "noreferrer noopener",
|
1586
1617
|
children: "DID Connect"
|
1587
1618
|
}),
|
1588
1619
|
description: t('products.didConnect.description'),
|
@@ -1590,26 +1621,6 @@ export default function Products({
|
|
1590
1621
|
}]]
|
1591
1622
|
}];
|
1592
1623
|
}, [t, locale]);
|
1593
|
-
|
1594
|
-
// 防止弹框超出 window
|
1595
|
-
useLayoutEffect(() => {
|
1596
|
-
const wrapper = wrapperRef.current;
|
1597
|
-
if (!wrapper) return;
|
1598
|
-
if (!isOpen) {
|
1599
|
-
wrapper.style.transform = '';
|
1600
|
-
return;
|
1601
|
-
}
|
1602
|
-
const rect = wrapper.getBoundingClientRect();
|
1603
|
-
const windowWidth = window.innerWidth;
|
1604
|
-
if (rect.right > windowWidth) {
|
1605
|
-
const offset = rect.right - windowWidth;
|
1606
|
-
wrapper.style.transform = `translateX(-${offset + 16}px)`;
|
1607
|
-
} else if (rect.left < 0) {
|
1608
|
-
wrapper.style.transform = `translateX(${Math.abs(rect.left) + 16}px)`;
|
1609
|
-
} else {
|
1610
|
-
wrapper.style.transform = '';
|
1611
|
-
}
|
1612
|
-
}, [width, height, isOpen]);
|
1613
1624
|
return /*#__PURE__*/_jsx(Wrapper, {
|
1614
1625
|
ref: wrapperRef,
|
1615
1626
|
className: `is-${mode} ${className}`,
|
package/lib/NavMenu/style.d.ts
CHANGED
@@ -2,8 +2,8 @@ type NavMenuProps = {
|
|
2
2
|
$bgColor: string;
|
3
3
|
$textColor: string;
|
4
4
|
};
|
5
|
-
export declare const
|
6
|
-
export declare const
|
5
|
+
export declare const NavMenuStyled: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & NavMenuProps, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, {}>;
|
6
|
+
export declare const NavMenuRoot: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme>, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>, {}>;
|
7
7
|
type NavMenuItemProps = {
|
8
8
|
$activeTextColor: string;
|
9
9
|
};
|
package/lib/NavMenu/style.js
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import { styled } from '../Theme';
|
2
|
-
// .navmenu
|
3
|
-
export const
|
2
|
+
// .navmenu
|
3
|
+
export const NavMenuStyled = styled('nav', {
|
4
4
|
shouldForwardProp: prop => prop !== '$bgColor' && prop !== '$textColor'
|
5
5
|
})(({
|
6
6
|
$bgColor,
|
7
7
|
$textColor
|
8
8
|
}) => ({
|
9
|
-
|
9
|
+
position: 'relative',
|
10
|
+
padding: '0 16px',
|
10
11
|
minWidth: '50px',
|
11
12
|
// FIXME: @zhanghan 这个只是临时的解决方案,会导致 header align right 不能真正的右对齐,需要修改 header 才能真正解决这个问题
|
12
13
|
flexGrow: 100,
|
@@ -15,15 +16,15 @@ export const NavMenuRoot = styled('nav', {
|
|
15
16
|
fontSize: '16px'
|
16
17
|
}));
|
17
18
|
|
18
|
-
// .navmenu-
|
19
|
-
export const
|
19
|
+
// .navmenu-root
|
20
|
+
export const NavMenuRoot = styled('ul')(() => ({
|
20
21
|
listStyle: 'none',
|
21
22
|
margin: 0,
|
22
23
|
padding: 0,
|
23
24
|
display: 'flex',
|
24
25
|
alignItems: 'center',
|
25
26
|
// inline 布局
|
26
|
-
'&.navmenu-
|
27
|
+
'&.navmenu-root--inline ': {
|
27
28
|
flexDirection: 'column',
|
28
29
|
alignItems: 'stretch'
|
29
30
|
}
|
@@ -32,15 +33,18 @@ export const NavMenuList = styled('ul')(() => ({
|
|
32
33
|
export const NavMenuItem = styled('li', {
|
33
34
|
shouldForwardProp: prop => prop !== '$activeTextColor'
|
34
35
|
})(({
|
35
|
-
$activeTextColor
|
36
|
+
$activeTextColor,
|
37
|
+
theme
|
36
38
|
}) => ({
|
37
39
|
display: 'flex',
|
38
40
|
alignItems: 'center',
|
39
41
|
position: 'relative',
|
40
|
-
padding: '
|
42
|
+
padding: '8px 12px',
|
41
43
|
whiteSpace: 'nowrap',
|
42
44
|
cursor: 'pointer',
|
43
|
-
transition: 'color
|
45
|
+
transition: theme.transitions.create('color', {
|
46
|
+
duration: theme.transitions.duration.standard
|
47
|
+
}),
|
44
48
|
// 间距调整
|
45
49
|
'&:first-of-type': {
|
46
50
|
paddingLeft: 0
|
@@ -93,7 +97,9 @@ export const NavMenuItem = styled('li', {
|
|
93
97
|
marginLeft: '6px',
|
94
98
|
fontSize: '14px',
|
95
99
|
opacity: 0,
|
96
|
-
transition: 'opacity
|
100
|
+
transition: theme.transitions.create('opacity', {
|
101
|
+
duration: theme.transitions.duration.standard
|
102
|
+
})
|
97
103
|
}
|
98
104
|
},
|
99
105
|
'.navmenu-item__desc': {
|
@@ -131,13 +137,17 @@ export const NavMenuItem = styled('li', {
|
|
131
137
|
},
|
132
138
|
'&:hover': {
|
133
139
|
background: '#f9f9fb',
|
134
|
-
transition: 'background
|
140
|
+
transition: theme.transitions.create('background', {
|
141
|
+
duration: theme.transitions.duration.standard
|
142
|
+
}),
|
135
143
|
'.navmenu-item__label-arrow': {
|
136
144
|
opacity: 1
|
137
145
|
},
|
138
146
|
'.navmenu-item__desc': {
|
139
147
|
color: '#26292e',
|
140
|
-
transition: 'color
|
148
|
+
transition: theme.transitions.create('color', {
|
149
|
+
duration: theme.transitions.duration.standard
|
150
|
+
})
|
141
151
|
}
|
142
152
|
},
|
143
153
|
'&.navmenu-item--active': {
|
@@ -155,24 +165,28 @@ export const NavMenuItem = styled('li', {
|
|
155
165
|
}));
|
156
166
|
|
157
167
|
// 包含子菜单的导航项 .navmenu-item .navmenu-sub
|
158
|
-
export const NavMenuSub = styled(NavMenuItem)((
|
168
|
+
export const NavMenuSub = styled(NavMenuItem)(({
|
169
|
+
theme
|
170
|
+
}) => ({
|
159
171
|
'.navmenu-item': {
|
160
|
-
marginLeft: 0
|
172
|
+
marginLeft: 0,
|
173
|
+
overflow: 'hidden'
|
161
174
|
},
|
162
175
|
'& .navmenu-sub__container': {
|
163
|
-
|
176
|
+
pointerEvents: 'none',
|
164
177
|
opacity: 0,
|
165
178
|
position: 'absolute',
|
166
179
|
top: '100%',
|
167
|
-
left:
|
168
|
-
zIndex: 1
|
169
|
-
transform: 'translateX(-50%)',
|
170
|
-
paddingTop: '16px',
|
171
|
-
transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out'
|
180
|
+
left: 0,
|
181
|
+
zIndex: 1
|
172
182
|
},
|
173
183
|
'&.navmenu-sub--opened > .navmenu-sub__container': {
|
174
|
-
|
175
|
-
opacity: 1
|
184
|
+
pointerEvents: 'auto',
|
185
|
+
opacity: 1,
|
186
|
+
transition: theme.transitions.create('opacity', {
|
187
|
+
duration: theme.transitions.duration.standard,
|
188
|
+
easing: theme.transitions.easing.easeOut
|
189
|
+
})
|
176
190
|
},
|
177
191
|
// 扩展图标
|
178
192
|
'.navmenu-sub__expand-icon': {
|
@@ -183,7 +197,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
183
197
|
'& > *': {
|
184
198
|
width: '0.8em',
|
185
199
|
height: '0.8em',
|
186
|
-
transition: 'transform
|
200
|
+
transition: theme.transitions.create('transform', {
|
201
|
+
duration: theme.transitions.duration.standard
|
202
|
+
})
|
187
203
|
},
|
188
204
|
'.navmenu-item--inline &': {
|
189
205
|
marginLeft: 'auto'
|
@@ -199,7 +215,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
199
215
|
padding: 0,
|
200
216
|
height: 0,
|
201
217
|
overflow: 'hidden',
|
202
|
-
transition: 'height
|
218
|
+
transition: theme.transitions.create('height', {
|
219
|
+
duration: theme.transitions.duration.standard
|
220
|
+
})
|
203
221
|
},
|
204
222
|
'&.navmenu-sub--opened > .navmenu-sub__container': {
|
205
223
|
height: 'auto'
|
@@ -208,11 +226,14 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
208
226
|
padding: 0,
|
209
227
|
paddingLeft: '16px',
|
210
228
|
boxShadow: 'none'
|
229
|
+
},
|
230
|
+
'.navmenu-item__content': {
|
231
|
+
height: '48px'
|
211
232
|
}
|
212
233
|
}
|
213
234
|
}));
|
214
235
|
|
215
|
-
// 下拉子菜单 .navmenu-
|
236
|
+
// 下拉子菜单 .navmenu-sub__list
|
216
237
|
export const NavMenuSubList = styled('ul')(() => ({
|
217
238
|
margin: 0,
|
218
239
|
padding: '16px',
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
interface SubContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
3
|
+
children: React.ReactNode;
|
4
|
+
}
|
5
|
+
export declare function SubContainer({ children, ...props }: SubContainerProps): import("react/jsx-runtime").JSX.Element;
|
6
|
+
export default SubContainer;
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
3
|
+
const paddingTop = 8;
|
4
|
+
|
5
|
+
// 下拉子菜单容器
|
6
|
+
export function SubContainer({
|
7
|
+
children,
|
8
|
+
...props
|
9
|
+
}) {
|
10
|
+
const rootRef = useRef(null);
|
11
|
+
const [position, setPosition] = useState(0); // 只需要保存水平偏移量
|
12
|
+
|
13
|
+
const updatePosition = () => {
|
14
|
+
if (!rootRef.current) return;
|
15
|
+
const anchor = rootRef.current.parentElement; // 以父容器作为参照
|
16
|
+
if (!anchor) return;
|
17
|
+
const anchorRect = anchor.getBoundingClientRect();
|
18
|
+
const containerRect = rootRef.current.getBoundingClientRect();
|
19
|
+
const windowWidth = window.innerWidth;
|
20
|
+
const padding = 32;
|
21
|
+
|
22
|
+
// 1. 计算相对于父元素的水平居中位置
|
23
|
+
let left = (anchorRect.width - containerRect.width) / 2;
|
24
|
+
|
25
|
+
// 2. 检查容器在视口中的绝对位置
|
26
|
+
const absoluteLeft = anchorRect.left + left;
|
27
|
+
|
28
|
+
// 3. 调整水平位置确保不超出窗口
|
29
|
+
if (absoluteLeft < 0) {
|
30
|
+
// 如果超出左边界,向右偏移
|
31
|
+
left = left - absoluteLeft + padding;
|
32
|
+
} else if (absoluteLeft + containerRect.width > windowWidth) {
|
33
|
+
// 如果超出右边界,向左偏移
|
34
|
+
left -= absoluteLeft + containerRect.width - windowWidth + padding;
|
35
|
+
}
|
36
|
+
setPosition(left);
|
37
|
+
};
|
38
|
+
|
39
|
+
// 监听自身大小变化
|
40
|
+
useEffect(() => {
|
41
|
+
const resizeObserver = new ResizeObserver(() => {
|
42
|
+
updatePosition();
|
43
|
+
});
|
44
|
+
resizeObserver.observe(rootRef.current);
|
45
|
+
return () => resizeObserver.disconnect();
|
46
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
47
|
+
}, []);
|
48
|
+
|
49
|
+
// 监听 anchor 的大小变化
|
50
|
+
useEffect(() => {
|
51
|
+
const anchor = rootRef.current?.parentElement;
|
52
|
+
let resizeObserver = null;
|
53
|
+
if (anchor) {
|
54
|
+
resizeObserver = new ResizeObserver(() => {
|
55
|
+
updatePosition();
|
56
|
+
});
|
57
|
+
resizeObserver.observe(anchor);
|
58
|
+
}
|
59
|
+
return () => resizeObserver?.disconnect();
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
61
|
+
}, []);
|
62
|
+
|
63
|
+
// 监听窗口大小变化
|
64
|
+
useEffect(() => {
|
65
|
+
const handleResize = () => {
|
66
|
+
updatePosition();
|
67
|
+
};
|
68
|
+
window.addEventListener('resize', handleResize);
|
69
|
+
return () => window.removeEventListener('resize', handleResize);
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
71
|
+
}, []);
|
72
|
+
return /*#__PURE__*/_jsx("div", {
|
73
|
+
ref: rootRef,
|
74
|
+
className: "navmenu-sub__container",
|
75
|
+
style: {
|
76
|
+
paddingTop: `${paddingTop}px`,
|
77
|
+
transform: `translateX(${position}px)`
|
78
|
+
},
|
79
|
+
...props,
|
80
|
+
children: children
|
81
|
+
});
|
82
|
+
}
|
83
|
+
export default SubContainer;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@arcblock/ux",
|
3
|
-
"version": "2.12.
|
3
|
+
"version": "2.12.4",
|
4
4
|
"description": "Common used react components for arcblock products",
|
5
5
|
"keywords": [
|
6
6
|
"react",
|
@@ -68,12 +68,12 @@
|
|
68
68
|
"react": ">=18.2.0",
|
69
69
|
"react-router-dom": ">=6.22.3"
|
70
70
|
},
|
71
|
-
"gitHead": "
|
71
|
+
"gitHead": "f8648a2f55bb57212a24a5948a5f026e2c20b1a7",
|
72
72
|
"dependencies": {
|
73
73
|
"@arcblock/did-motif": "^1.1.13",
|
74
|
-
"@arcblock/icons": "^2.12.
|
75
|
-
"@arcblock/nft-display": "^2.12.
|
76
|
-
"@arcblock/react-hooks": "^2.12.
|
74
|
+
"@arcblock/icons": "^2.12.4",
|
75
|
+
"@arcblock/nft-display": "^2.12.4",
|
76
|
+
"@arcblock/react-hooks": "^2.12.4",
|
77
77
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
78
78
|
"@fontsource/inter": "^5.0.16",
|
79
79
|
"@fontsource/ubuntu-mono": "^5.0.18",
|
package/src/NavMenu/nav-menu.tsx
CHANGED
@@ -5,7 +5,8 @@ import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuI
|
|
5
5
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
6
6
|
import { useCreation, useMemoizedFn, useReactive, useSize, useThrottleFn } from 'ahooks';
|
7
7
|
import { NavMenuProvider, useNavMenuContext } from './nav-menu-context';
|
8
|
-
import { NavMenuRoot,
|
8
|
+
import { NavMenuRoot, NavMenuItem, NavMenuSub, NavMenuSubList, NavMenuStyled } from './style';
|
9
|
+
import { SubContainer } from './sub-container';
|
9
10
|
|
10
11
|
// 过滤 children 中的 Item/Sub, 忽略其它类型的 element
|
11
12
|
function filterItems(children: React.ReactNode) {
|
@@ -67,7 +68,6 @@ function NavMenu({
|
|
67
68
|
if (!items?.length && !children?.length) {
|
68
69
|
throw new Error("One of 'items' or 'children' is required by NavMenu component.");
|
69
70
|
}
|
70
|
-
|
71
71
|
const currentState = useReactive({
|
72
72
|
activeId,
|
73
73
|
openedIds: [] as string[],
|
@@ -112,6 +112,7 @@ function NavMenu({
|
|
112
112
|
const navMenuRef = useRef<HTMLUListElement | null>(null);
|
113
113
|
const itemRefs = useRef<HTMLElement[]>([]);
|
114
114
|
const moreIconRef = useRef<HTMLLIElement | null>(null);
|
115
|
+
const containerWidth = useRef(0);
|
115
116
|
const isAllItemsHidden = currentState.hiddenItemCount === itemRefs.current?.length;
|
116
117
|
const style = isAllItemsHidden ? { marginLeft: '0px' } : undefined;
|
117
118
|
|
@@ -136,7 +137,6 @@ function NavMenu({
|
|
136
137
|
let totalWidthUsed = 0;
|
137
138
|
let newHiddenCount = 0;
|
138
139
|
let leftAllHidden = false;
|
139
|
-
const containerWidth = containerSize?.width || 0;
|
140
140
|
const moreIconWidth = moreIconRef.current
|
141
141
|
? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft)
|
142
142
|
: 0;
|
@@ -147,7 +147,7 @@ function NavMenu({
|
|
147
147
|
const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
|
148
148
|
const currentItemWidth = item.offsetWidth + marginLeft;
|
149
149
|
|
150
|
-
if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
|
150
|
+
if (containerWidth.current - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
|
151
151
|
totalWidthUsed += currentItemWidth;
|
152
152
|
} else {
|
153
153
|
item.style.display = 'none';
|
@@ -165,6 +165,7 @@ function NavMenu({
|
|
165
165
|
);
|
166
166
|
|
167
167
|
useLayoutEffect(() => {
|
168
|
+
containerWidth.current = containerSize?.width || navMenuRef.current?.clientWidth || 0;
|
168
169
|
if (mode === 'horizontal') {
|
169
170
|
checkItemsFit();
|
170
171
|
}
|
@@ -195,8 +196,15 @@ function NavMenu({
|
|
195
196
|
|
196
197
|
const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
|
197
198
|
|
198
|
-
const renderItem = (item: ItemOptions, index: number,
|
199
|
+
const renderItem = (item: ItemOptions, index: number, level = 0) => {
|
200
|
+
const isTopLevel = level === 0;
|
201
|
+
|
199
202
|
if (item?.children) {
|
203
|
+
// 只渲染两级子菜单
|
204
|
+
if (level > 0) {
|
205
|
+
return null;
|
206
|
+
}
|
207
|
+
|
200
208
|
// 对于 Sub 组件,如果它是顶级组件,则包含 ref
|
201
209
|
return (
|
202
210
|
<Sub
|
@@ -215,7 +223,7 @@ function NavMenu({
|
|
215
223
|
{typeof item.children === 'function'
|
216
224
|
? item.children
|
217
225
|
: item.children.map((childItem, childIndex: number) =>
|
218
|
-
renderItem({ ...childItem, variant: 'panel' }, childIndex,
|
226
|
+
renderItem({ ...childItem, variant: 'panel' }, childIndex, level + 1)
|
219
227
|
)}
|
220
228
|
</Sub>
|
221
229
|
);
|
@@ -243,21 +251,24 @@ function NavMenu({
|
|
243
251
|
};
|
244
252
|
|
245
253
|
const content = items
|
246
|
-
? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index))
|
254
|
+
? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index, 1))
|
247
255
|
: children?.slice(-currentState.hiddenItemCount);
|
248
256
|
|
257
|
+
// 当前展开的子菜单
|
258
|
+
const openedId = currentState.openedIds[0];
|
259
|
+
|
249
260
|
return (
|
250
261
|
<NavMenuProvider value={contextValue}>
|
251
|
-
<
|
252
|
-
<
|
253
|
-
{items ? items.map((item, index) => renderItem(item, index
|
262
|
+
<NavMenuStyled {...rest} className={classes} $textColor={textColor} $bgColor={bgColor}>
|
263
|
+
<NavMenuRoot className={clsx('navmenu-root', `navmenu-root--${mode}`)} ref={navMenuRef}>
|
264
|
+
{items ? items.map((item, index) => renderItem(item, index)) : renderChildrenWithRef(children || [])}
|
254
265
|
{currentState.hiddenItemCount > 0 && (
|
255
266
|
<Sub expandIcon={false} icon={icon} label="" ref={moreIconRef} style={style}>
|
256
267
|
{content}
|
257
268
|
</Sub>
|
258
269
|
)}
|
259
|
-
</
|
260
|
-
</
|
270
|
+
</NavMenuRoot>
|
271
|
+
</NavMenuStyled>
|
261
272
|
</NavMenuProvider>
|
262
273
|
);
|
263
274
|
}
|
@@ -324,7 +335,7 @@ export const Item = forwardRef<HTMLLIElement, ItemProps>(
|
|
324
335
|
Item.displayName = 'NavMenu.Item';
|
325
336
|
|
326
337
|
export interface SubProps extends Omit<ItemProps, 'children' | 'active'> {
|
327
|
-
children?: Array<React.ReactElement> | ((props: { isOpen: boolean }) => React.ReactElement | null);
|
338
|
+
children?: Array<React.ReactElement | null> | ((props: { isOpen: boolean }) => React.ReactElement | null);
|
328
339
|
expandIcon?: React.ReactNode | ((props: { isOpen: boolean }) => React.ReactNode);
|
329
340
|
}
|
330
341
|
|
@@ -394,13 +405,13 @@ export const Sub = forwardRef<HTMLLIElement, SubProps>(
|
|
394
405
|
{typeof expandIcon === 'function' ? expandIcon({ isOpen }) : expandIcon}
|
395
406
|
</span>
|
396
407
|
)}
|
397
|
-
<
|
408
|
+
<SubContainer {...containerProps}>
|
398
409
|
{typeof children === 'function' ? (
|
399
410
|
children({ isOpen }) // 自定义渲染
|
400
411
|
) : (
|
401
412
|
<NavMenuSubList className="navmenu-sub__list">{filterItems(children)}</NavMenuSubList>
|
402
413
|
)}
|
403
|
-
</
|
414
|
+
</SubContainer>
|
404
415
|
</NavMenuSub>
|
405
416
|
);
|
406
417
|
}
|
package/src/NavMenu/products.tsx
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
import {
|
1
|
+
import { useRef } from 'react';
|
2
2
|
import { Link } from 'react-router-dom';
|
3
3
|
import { useCreation, useMemoizedFn } from 'ahooks';
|
4
4
|
import { Box, BoxProps, Grid } from '@mui/material';
|
5
|
-
import { useWindowSize } from 'react-use';
|
6
5
|
import SubItemGroup from './sub-item-group';
|
7
6
|
import { Item } from './nav-menu';
|
8
7
|
import { styled } from '../Theme';
|
@@ -191,7 +190,6 @@ export interface ProductsProps extends BoxProps {
|
|
191
190
|
export default function Products({ className, isOpen, ...rest }: ProductsProps) {
|
192
191
|
const { mode } = useNavMenuContext();
|
193
192
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
194
|
-
const { width, height } = useWindowSize();
|
195
193
|
const { locale = 'en' } = useLocaleContext() || {};
|
196
194
|
const t = useMemoizedFn((key, data = {}) => translate(translations, key, locale, 'en', data));
|
197
195
|
const groups = useCreation(() => {
|
@@ -202,13 +200,22 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
202
200
|
children: [
|
203
201
|
[
|
204
202
|
{
|
205
|
-
label:
|
203
|
+
label: (
|
204
|
+
<Link to={`https://www.nftstudio.rocks/${locale}`} target="_blank" rel="noreferrer noopener">
|
205
|
+
NFT Studio
|
206
|
+
</Link>
|
207
|
+
),
|
206
208
|
description: t('products.nftStudio.description'),
|
207
209
|
icon: <NftStudioSvg />,
|
208
210
|
},
|
209
211
|
{
|
210
212
|
label: (
|
211
|
-
<Link
|
213
|
+
<Link
|
214
|
+
to={`https://www.arcblock.io/content/collections/${locale}/creator-studio`}
|
215
|
+
target="_blank"
|
216
|
+
rel="noreferrer noopener">
|
217
|
+
Creator Studio
|
218
|
+
</Link>
|
212
219
|
),
|
213
220
|
description: t('products.creatorStudio.description'),
|
214
221
|
icon: <CreatorStudioSvg />,
|
@@ -216,12 +223,20 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
216
223
|
],
|
217
224
|
[
|
218
225
|
{
|
219
|
-
label:
|
226
|
+
label: (
|
227
|
+
<Link to={`https://www.aigne.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
228
|
+
AIGNE
|
229
|
+
</Link>
|
230
|
+
),
|
220
231
|
description: t('products.aigne.description'),
|
221
232
|
icon: <AigneSvg />,
|
222
233
|
},
|
223
234
|
{
|
224
|
-
label:
|
235
|
+
label: (
|
236
|
+
<Link to={`https://www.aistro.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
237
|
+
Aistro
|
238
|
+
</Link>
|
239
|
+
),
|
225
240
|
description: t('products.aistro.description'),
|
226
241
|
icon: <AistroSvg />,
|
227
242
|
},
|
@@ -234,24 +249,43 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
234
249
|
children: [
|
235
250
|
[
|
236
251
|
{
|
237
|
-
label:
|
252
|
+
label: (
|
253
|
+
<Link to={`https://launcher.arcblock.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
254
|
+
Blocklet Launcher
|
255
|
+
</Link>
|
256
|
+
),
|
238
257
|
description: t('products.blockletLauncher.description'),
|
239
258
|
icon: <BlockletLauncherSvg />,
|
240
259
|
},
|
241
260
|
{
|
242
|
-
label:
|
261
|
+
label: (
|
262
|
+
<Link
|
263
|
+
to={`https://www.arcblock.io/content/collections/${locale}/ai-kit`}
|
264
|
+
target="_blank"
|
265
|
+
rel="noreferrer noopener">
|
266
|
+
Al Kit
|
267
|
+
</Link>
|
268
|
+
),
|
243
269
|
description: t('products.alKit.description'),
|
244
270
|
icon: <AIKitSvg />,
|
245
271
|
},
|
246
272
|
],
|
247
273
|
[
|
248
274
|
{
|
249
|
-
label:
|
275
|
+
label: (
|
276
|
+
<Link to={`https://store.blocklet.dev/${locale}`} target="_blank" rel="noreferrer noopener">
|
277
|
+
Blocklet Store
|
278
|
+
</Link>
|
279
|
+
),
|
250
280
|
description: t('products.blockletStore.description'),
|
251
281
|
icon: <BlockletStoreSvg />,
|
252
282
|
},
|
253
283
|
{
|
254
|
-
label:
|
284
|
+
label: (
|
285
|
+
<Link to={`https://www.web3kit.rocks/${locale}`} target="_blank" rel="noreferrer noopener">
|
286
|
+
Web3 Kit
|
287
|
+
</Link>
|
288
|
+
),
|
255
289
|
description: t('products.web3Kit.description'),
|
256
290
|
icon: <Web3KitSvg />,
|
257
291
|
},
|
@@ -265,18 +299,31 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
265
299
|
[
|
266
300
|
{
|
267
301
|
label: (
|
268
|
-
<Link
|
302
|
+
<Link
|
303
|
+
to={`https://www.arcblock.io/content/collections/${locale}/blocklet`}
|
304
|
+
target="_blank"
|
305
|
+
rel="noreferrer noopener">
|
306
|
+
Blocklet Framework
|
307
|
+
</Link>
|
269
308
|
),
|
270
309
|
description: t('products.blockletFramework.description'),
|
271
310
|
icon: <BlockletFrameworkSvg />,
|
272
311
|
},
|
273
312
|
{
|
274
|
-
label:
|
313
|
+
label: (
|
314
|
+
<Link to={`https://www.didspaces.com/${locale}`} target="_blank" rel="noreferrer noopener">
|
315
|
+
DID Spaces
|
316
|
+
</Link>
|
317
|
+
),
|
275
318
|
description: t('products.didSpaces.description'),
|
276
319
|
icon: <DidSvg />,
|
277
320
|
},
|
278
321
|
{
|
279
|
-
label:
|
322
|
+
label: (
|
323
|
+
<Link to={`https://main.abtnetwork.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
324
|
+
ABT Network
|
325
|
+
</Link>
|
326
|
+
),
|
280
327
|
description: t('products.abtNetwork.description'),
|
281
328
|
icon: <AbtNetworkSvg />,
|
282
329
|
},
|
@@ -284,7 +331,10 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
284
331
|
[
|
285
332
|
{
|
286
333
|
label: (
|
287
|
-
<Link
|
334
|
+
<Link
|
335
|
+
to={`https://www.arcblock.io/content/collections/${locale}/blocklet-server`}
|
336
|
+
target="_blank"
|
337
|
+
rel="noreferrer noopener">
|
288
338
|
Blocklet Server
|
289
339
|
</Link>
|
290
340
|
),
|
@@ -292,7 +342,14 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
292
342
|
icon: <BlockletServerSvg />,
|
293
343
|
},
|
294
344
|
{
|
295
|
-
label:
|
345
|
+
label: (
|
346
|
+
<Link
|
347
|
+
to={`https://www.arcblock.io/content/collections/${locale}/blockchain`}
|
348
|
+
target="_blank"
|
349
|
+
rel="noreferrer noopener">
|
350
|
+
ОСАР
|
351
|
+
</Link>
|
352
|
+
),
|
296
353
|
description: t('products.osar.description'),
|
297
354
|
icon: <OCAPSvg />,
|
298
355
|
},
|
@@ -305,29 +362,55 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
305
362
|
children: [
|
306
363
|
[
|
307
364
|
{
|
308
|
-
label:
|
365
|
+
label: (
|
366
|
+
<Link
|
367
|
+
to={`https://www.arcblock.io/content/collections/${locale}/did`}
|
368
|
+
target="_blank"
|
369
|
+
rel="noreferrer noopener">
|
370
|
+
DID
|
371
|
+
</Link>
|
372
|
+
),
|
309
373
|
description: t('products.did.description'),
|
310
374
|
icon: <DidSvg />,
|
311
375
|
},
|
312
376
|
{
|
313
|
-
label:
|
377
|
+
label: (
|
378
|
+
<Link to={`https://www.didwallet.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
379
|
+
DID Wallet
|
380
|
+
</Link>
|
381
|
+
),
|
314
382
|
description: t('products.didWallet.description'),
|
315
383
|
icon: <DidWalletSvg />,
|
316
384
|
},
|
317
385
|
{
|
318
|
-
label:
|
386
|
+
label: (
|
387
|
+
<Link to={`https://www.didnames.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
388
|
+
DID Names
|
389
|
+
</Link>
|
390
|
+
),
|
319
391
|
description: t('products.didNameService.description'),
|
320
392
|
icon: <DidNameServiceSvg />,
|
321
393
|
},
|
322
394
|
],
|
323
395
|
[
|
324
396
|
{
|
325
|
-
label:
|
397
|
+
label: (
|
398
|
+
<Link
|
399
|
+
to={`https://www.arcblock.io/content/collections/${locale}/verifiable-credential`}
|
400
|
+
target="_blank"
|
401
|
+
rel="noreferrer noopener">
|
402
|
+
VC
|
403
|
+
</Link>
|
404
|
+
),
|
326
405
|
description: t('products.vc.description'),
|
327
406
|
icon: <VCSvg />,
|
328
407
|
},
|
329
408
|
{
|
330
|
-
label:
|
409
|
+
label: (
|
410
|
+
<Link to={`https://www.didconnect.io/${locale}`} target="_blank" rel="noreferrer noopener">
|
411
|
+
DID Connect
|
412
|
+
</Link>
|
413
|
+
),
|
331
414
|
description: t('products.didConnect.description'),
|
332
415
|
icon: <DidConnectSvg />,
|
333
416
|
},
|
@@ -337,27 +420,6 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
|
|
337
420
|
];
|
338
421
|
}, [t, locale]);
|
339
422
|
|
340
|
-
// 防止弹框超出 window
|
341
|
-
useLayoutEffect(() => {
|
342
|
-
const wrapper = wrapperRef.current;
|
343
|
-
if (!wrapper) return;
|
344
|
-
if (!isOpen) {
|
345
|
-
wrapper.style.transform = '';
|
346
|
-
return;
|
347
|
-
}
|
348
|
-
|
349
|
-
const rect = wrapper.getBoundingClientRect();
|
350
|
-
const windowWidth = window.innerWidth;
|
351
|
-
if (rect.right > windowWidth) {
|
352
|
-
const offset = rect.right - windowWidth;
|
353
|
-
wrapper.style.transform = `translateX(-${offset + 16}px)`;
|
354
|
-
} else if (rect.left < 0) {
|
355
|
-
wrapper.style.transform = `translateX(${Math.abs(rect.left) + 16}px)`;
|
356
|
-
} else {
|
357
|
-
wrapper.style.transform = '';
|
358
|
-
}
|
359
|
-
}, [width, height, isOpen]);
|
360
|
-
|
361
423
|
return (
|
362
424
|
<Wrapper ref={wrapperRef} className={`is-${mode} ${className}`} {...rest}>
|
363
425
|
{groups.map((group) => (
|
package/src/NavMenu/style.ts
CHANGED
@@ -5,11 +5,12 @@ type NavMenuProps = {
|
|
5
5
|
$textColor: string;
|
6
6
|
};
|
7
7
|
|
8
|
-
// .navmenu
|
9
|
-
export const
|
8
|
+
// .navmenu
|
9
|
+
export const NavMenuStyled = styled('nav', {
|
10
10
|
shouldForwardProp: (prop) => prop !== '$bgColor' && prop !== '$textColor',
|
11
11
|
})<NavMenuProps>(({ $bgColor, $textColor }) => ({
|
12
|
-
|
12
|
+
position: 'relative',
|
13
|
+
padding: '0 16px',
|
13
14
|
minWidth: '50px',
|
14
15
|
// FIXME: @zhanghan 这个只是临时的解决方案,会导致 header align right 不能真正的右对齐,需要修改 header 才能真正解决这个问题
|
15
16
|
flexGrow: 100,
|
@@ -18,15 +19,15 @@ export const NavMenuRoot = styled('nav', {
|
|
18
19
|
fontSize: '16px',
|
19
20
|
}));
|
20
21
|
|
21
|
-
// .navmenu-
|
22
|
-
export const
|
22
|
+
// .navmenu-root
|
23
|
+
export const NavMenuRoot = styled('ul')(() => ({
|
23
24
|
listStyle: 'none',
|
24
25
|
margin: 0,
|
25
26
|
padding: 0,
|
26
27
|
display: 'flex',
|
27
28
|
alignItems: 'center',
|
28
29
|
// inline 布局
|
29
|
-
'&.navmenu-
|
30
|
+
'&.navmenu-root--inline ': {
|
30
31
|
flexDirection: 'column',
|
31
32
|
alignItems: 'stretch',
|
32
33
|
},
|
@@ -38,14 +39,16 @@ type NavMenuItemProps = {
|
|
38
39
|
// 菜单项 .navmenu-item
|
39
40
|
export const NavMenuItem = styled('li', {
|
40
41
|
shouldForwardProp: (prop) => prop !== '$activeTextColor',
|
41
|
-
})<NavMenuItemProps>(({ $activeTextColor }) => ({
|
42
|
+
})<NavMenuItemProps>(({ $activeTextColor, theme }) => ({
|
42
43
|
display: 'flex',
|
43
44
|
alignItems: 'center',
|
44
45
|
position: 'relative',
|
45
|
-
padding: '
|
46
|
+
padding: '8px 12px',
|
46
47
|
whiteSpace: 'nowrap',
|
47
48
|
cursor: 'pointer',
|
48
|
-
transition: 'color
|
49
|
+
transition: theme.transitions.create('color', {
|
50
|
+
duration: theme.transitions.duration.standard,
|
51
|
+
}),
|
49
52
|
// 间距调整
|
50
53
|
'&:first-of-type': {
|
51
54
|
paddingLeft: 0,
|
@@ -98,7 +101,9 @@ export const NavMenuItem = styled('li', {
|
|
98
101
|
marginLeft: '6px',
|
99
102
|
fontSize: '14px',
|
100
103
|
opacity: 0,
|
101
|
-
transition: 'opacity
|
104
|
+
transition: theme.transitions.create('opacity', {
|
105
|
+
duration: theme.transitions.duration.standard,
|
106
|
+
}),
|
102
107
|
},
|
103
108
|
},
|
104
109
|
'.navmenu-item__desc': {
|
@@ -136,13 +141,17 @@ export const NavMenuItem = styled('li', {
|
|
136
141
|
},
|
137
142
|
'&:hover': {
|
138
143
|
background: '#f9f9fb',
|
139
|
-
transition: 'background
|
144
|
+
transition: theme.transitions.create('background', {
|
145
|
+
duration: theme.transitions.duration.standard,
|
146
|
+
}),
|
140
147
|
'.navmenu-item__label-arrow': {
|
141
148
|
opacity: 1,
|
142
149
|
},
|
143
150
|
'.navmenu-item__desc': {
|
144
151
|
color: '#26292e',
|
145
|
-
transition: 'color
|
152
|
+
transition: theme.transitions.create('color', {
|
153
|
+
duration: theme.transitions.duration.standard,
|
154
|
+
}),
|
146
155
|
},
|
147
156
|
},
|
148
157
|
'&.navmenu-item--active': {
|
@@ -160,24 +169,26 @@ export const NavMenuItem = styled('li', {
|
|
160
169
|
}));
|
161
170
|
|
162
171
|
// 包含子菜单的导航项 .navmenu-item .navmenu-sub
|
163
|
-
export const NavMenuSub = styled(NavMenuItem)(() => ({
|
172
|
+
export const NavMenuSub = styled(NavMenuItem)(({ theme }) => ({
|
164
173
|
'.navmenu-item': {
|
165
174
|
marginLeft: 0,
|
175
|
+
overflow: 'hidden',
|
166
176
|
},
|
167
177
|
'& .navmenu-sub__container': {
|
168
|
-
|
178
|
+
pointerEvents: 'none',
|
169
179
|
opacity: 0,
|
170
180
|
position: 'absolute',
|
171
181
|
top: '100%',
|
172
|
-
left:
|
182
|
+
left: 0,
|
173
183
|
zIndex: 1,
|
174
|
-
transform: 'translateX(-50%)',
|
175
|
-
paddingTop: '16px',
|
176
|
-
transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
|
177
184
|
},
|
178
185
|
'&.navmenu-sub--opened > .navmenu-sub__container': {
|
179
|
-
|
186
|
+
pointerEvents: 'auto',
|
180
187
|
opacity: 1,
|
188
|
+
transition: theme.transitions.create('opacity', {
|
189
|
+
duration: theme.transitions.duration.standard,
|
190
|
+
easing: theme.transitions.easing.easeOut,
|
191
|
+
}),
|
181
192
|
},
|
182
193
|
// 扩展图标
|
183
194
|
'.navmenu-sub__expand-icon': {
|
@@ -188,7 +199,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
188
199
|
'& > *': {
|
189
200
|
width: '0.8em',
|
190
201
|
height: '0.8em',
|
191
|
-
transition: 'transform
|
202
|
+
transition: theme.transitions.create('transform', {
|
203
|
+
duration: theme.transitions.duration.standard,
|
204
|
+
}),
|
192
205
|
},
|
193
206
|
'.navmenu-item--inline &': {
|
194
207
|
marginLeft: 'auto',
|
@@ -204,7 +217,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
204
217
|
padding: 0,
|
205
218
|
height: 0,
|
206
219
|
overflow: 'hidden',
|
207
|
-
transition: 'height
|
220
|
+
transition: theme.transitions.create('height', {
|
221
|
+
duration: theme.transitions.duration.standard,
|
222
|
+
}),
|
208
223
|
},
|
209
224
|
'&.navmenu-sub--opened > .navmenu-sub__container': {
|
210
225
|
height: 'auto',
|
@@ -214,10 +229,13 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
|
|
214
229
|
paddingLeft: '16px',
|
215
230
|
boxShadow: 'none',
|
216
231
|
},
|
232
|
+
'.navmenu-item__content': {
|
233
|
+
height: '48px',
|
234
|
+
},
|
217
235
|
},
|
218
236
|
}));
|
219
237
|
|
220
|
-
// 下拉子菜单 .navmenu-
|
238
|
+
// 下拉子菜单 .navmenu-sub__list
|
221
239
|
export const NavMenuSubList = styled('ul')(() => ({
|
222
240
|
margin: 0,
|
223
241
|
padding: '16px',
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
2
|
+
|
3
|
+
interface SubContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
4
|
+
children: React.ReactNode;
|
5
|
+
}
|
6
|
+
|
7
|
+
const paddingTop = 8;
|
8
|
+
|
9
|
+
// 下拉子菜单容器
|
10
|
+
export function SubContainer({ children, ...props }: SubContainerProps) {
|
11
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
12
|
+
const [position, setPosition] = useState(0); // 只需要保存水平偏移量
|
13
|
+
|
14
|
+
const updatePosition = () => {
|
15
|
+
if (!rootRef.current) return;
|
16
|
+
const anchor = rootRef.current.parentElement; // 以父容器作为参照
|
17
|
+
if (!anchor) return;
|
18
|
+
|
19
|
+
const anchorRect = anchor.getBoundingClientRect();
|
20
|
+
const containerRect = rootRef.current.getBoundingClientRect();
|
21
|
+
const windowWidth = window.innerWidth;
|
22
|
+
const padding = 32;
|
23
|
+
|
24
|
+
// 1. 计算相对于父元素的水平居中位置
|
25
|
+
let left = (anchorRect.width - containerRect.width) / 2;
|
26
|
+
|
27
|
+
// 2. 检查容器在视口中的绝对位置
|
28
|
+
const absoluteLeft = anchorRect.left + left;
|
29
|
+
|
30
|
+
// 3. 调整水平位置确保不超出窗口
|
31
|
+
if (absoluteLeft < 0) {
|
32
|
+
// 如果超出左边界,向右偏移
|
33
|
+
left = left - absoluteLeft + padding;
|
34
|
+
} else if (absoluteLeft + containerRect.width > windowWidth) {
|
35
|
+
// 如果超出右边界,向左偏移
|
36
|
+
left -= absoluteLeft + containerRect.width - windowWidth + padding;
|
37
|
+
}
|
38
|
+
|
39
|
+
setPosition(left);
|
40
|
+
};
|
41
|
+
|
42
|
+
// 监听自身大小变化
|
43
|
+
useEffect(() => {
|
44
|
+
const resizeObserver = new ResizeObserver(() => {
|
45
|
+
updatePosition();
|
46
|
+
});
|
47
|
+
|
48
|
+
resizeObserver.observe(rootRef.current!);
|
49
|
+
|
50
|
+
return () => resizeObserver.disconnect();
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
52
|
+
}, []);
|
53
|
+
|
54
|
+
// 监听 anchor 的大小变化
|
55
|
+
useEffect(() => {
|
56
|
+
const anchor = rootRef.current?.parentElement;
|
57
|
+
let resizeObserver: ResizeObserver | null = null;
|
58
|
+
|
59
|
+
if (anchor) {
|
60
|
+
resizeObserver = new ResizeObserver(() => {
|
61
|
+
updatePosition();
|
62
|
+
});
|
63
|
+
|
64
|
+
resizeObserver.observe(anchor);
|
65
|
+
}
|
66
|
+
|
67
|
+
return () => resizeObserver?.disconnect();
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
69
|
+
}, []);
|
70
|
+
|
71
|
+
// 监听窗口大小变化
|
72
|
+
useEffect(() => {
|
73
|
+
const handleResize = () => {
|
74
|
+
updatePosition();
|
75
|
+
};
|
76
|
+
|
77
|
+
window.addEventListener('resize', handleResize);
|
78
|
+
return () => window.removeEventListener('resize', handleResize);
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
80
|
+
}, []);
|
81
|
+
|
82
|
+
return (
|
83
|
+
<div
|
84
|
+
ref={rootRef}
|
85
|
+
className="navmenu-sub__container"
|
86
|
+
style={{
|
87
|
+
paddingTop: `${paddingTop}px`,
|
88
|
+
transform: `translateX(${position}px)`,
|
89
|
+
}}
|
90
|
+
{...props}>
|
91
|
+
{children}
|
92
|
+
</div>
|
93
|
+
);
|
94
|
+
}
|
95
|
+
|
96
|
+
export default SubContainer;
|