diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx
index c1a04c14ea..dfe9539af6 100644
--- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx
+++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx
@@ -5,22 +5,23 @@ import { manualOptionsControl } from "comps/controls/optionsControl";
import { BoolCodeControl, StringControl, jsonControl, NumberControl } from "comps/controls/codeControl";
import { IconControl } from "comps/controls/iconControl";
import styled from "styled-components";
-import React, { Suspense, useContext, useEffect, useMemo, useState } from "react";
+import React, { Suspense, useContext, useEffect, useMemo, useState, useCallback } from "react";
import { registerLayoutMap } from "comps/comps/uiComp";
import { AppSelectComp } from "comps/comps/layout/appSelectComp";
import { NameAndExposingInfo } from "comps/utils/exposingTypes";
import { ConstructorToComp, ConstructorToDataType } from "lowcoder-core";
import { CanvasContainer } from "comps/comps/gridLayoutComp/canvasView";
import { CanvasContainerID } from "constants/domLocators";
+import { PreviewContainerID } from "constants/domLocators";
import { EditorContainer, EmptyContent } from "pages/common/styledComponent";
import { Layers } from "constants/Layers";
import { ExternalEditorContext } from "util/context/ExternalEditorContext";
import { default as Skeleton } from "antd/es/skeleton";
import { hiddenPropertyView } from "comps/utils/propertyUtils";
import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl";
-import { DataOption, DataOptionType, ModeOptions, menuItemStyleOptions, mobileNavJsonMenuItems } from "./navLayoutConstants";
+import { DataOption, DataOptionType, menuItemStyleOptions, mobileNavJsonMenuItems, MobileModeOptions, MobileMode, HamburgerPositionOptions, DrawerPlacementOptions } from "./navLayoutConstants";
import { styleControl } from "@lowcoder-ee/comps/controls/styleControl";
-import { NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants";
+import { HamburgerButtonStyle, DrawerContainerStyle, NavLayoutItemActiveStyle, NavLayoutItemActiveStyleType, NavLayoutItemHoverStyle, NavLayoutItemHoverStyleType, NavLayoutItemStyle, NavLayoutItemStyleType, NavLayoutStyle, NavLayoutStyleType } from "@lowcoder-ee/comps/controls/styleControlConstants";
import Segmented from "antd/es/segmented";
import { controlItem } from "components/control";
import { check } from "@lowcoder-ee/util/convertUtils";
@@ -30,10 +31,13 @@ import { ThemeContext } from "@lowcoder-ee/comps/utils/themeContext";
import { AlignCenter } from "lowcoder-design";
import { AlignLeft } from "lowcoder-design";
import { AlignRight } from "lowcoder-design";
+import { Drawer } from "lowcoder-design";
import { LayoutActionComp } from "./layoutActionComp";
import { defaultTheme } from "@lowcoder-ee/constants/themeConstants";
import { clickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl";
import { childrenToProps } from "@lowcoder-ee/comps/generators/multi";
+import { useAppPathParam } from "util/hooks";
+import { ALL_APPLICATIONS_URL } from "constants/routesURL";
const TabBar = React.lazy(() => import("antd-mobile/es/components/tab-bar"));
const TabBarItem = React.lazy(() =>
@@ -65,6 +69,139 @@ const TabLayoutViewContainer = styled.div<{
flex-direction: column;
`;
+const HamburgerButton = styled.button<{
+ $size: string;
+ $position: string; // bottom-right | bottom-left | top-right | top-left
+ $zIndex: number;
+ $background?: string;
+ $borderColor?: string;
+ $radius?: string;
+ $margin?: string;
+ $padding?: string;
+ $borderWidth?: string;
+}>`
+ position: fixed;
+ ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')}
+ ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')}
+ width: ${(props) => props.$size};
+ height: ${(props) => props.$size};
+ border-radius: ${(props) => props.$radius || '50%'};
+ border: ${(props) => props.$borderWidth || '1px'} solid ${(props) => props.$borderColor || 'rgba(0,0,0,0.1)'};
+ background: ${(props) => props.$background || 'white'};
+ margin: ${(props) => props.$margin || '0px'};
+ padding: ${(props) => props.$padding || '0px'};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: ${(props) => props.$zIndex};
+ cursor: pointer;
+ box-shadow: 0 6px 16px rgba(0,0,0,0.15);
+`;
+
+const BurgerIcon = styled.div<{
+ $lineColor?: string;
+}>`
+ width: 60%;
+ height: 2px;
+ background: ${(p) => p.$lineColor || '#333'};
+ position: relative;
+ &::before, &::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background: inherit;
+ }
+ &::before { top: -6px; }
+ &::after { top: 6px; }
+`;
+
+const IconWrapper = styled.div<{
+ $iconColor?: string;
+}>`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ svg {
+ color: ${(p) => p.$iconColor || 'inherit'};
+ fill: ${(p) => p.$iconColor || 'currentColor'};
+ }
+`;
+
+const DrawerContent = styled.div<{
+ $background: string;
+ $padding?: string;
+ $borderColor?: string;
+ $borderWidth?: string;
+ $margin?: string;
+}>`
+ background: ${(p) => p.$background};
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ padding: ${(p) => p.$padding || '12px'};
+ margin: ${(p) => p.$margin || '0px'};
+ box-sizing: border-box;
+ border: ${(p) => p.$borderWidth || '1px'} solid ${(p) => p.$borderColor || 'transparent'};
+`;
+
+const DrawerHeader = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+`;
+
+const DrawerCloseButton = styled.button<{
+ $color: string;
+}>`
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: ${(p) => p.$color};
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 16px;
+`;
+
+const DrawerList = styled.div<{
+ $itemStyle: NavLayoutItemStyleType;
+ $hoverStyle: NavLayoutItemHoverStyleType;
+ $activeStyle: NavLayoutItemActiveStyleType;
+}>`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .drawer-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background-color: ${(p) => p.$itemStyle.background};
+ color: ${(p) => p.$itemStyle.text};
+ border-radius: ${(p) => p.$itemStyle.radius};
+ border: 1px solid ${(p) => p.$itemStyle.border};
+ margin: ${(p) => p.$itemStyle.margin};
+ padding: ${(p) => p.$itemStyle.padding};
+ cursor: pointer;
+ user-select: none;
+ }
+ .drawer-item:hover {
+ background-color: ${(p) => p.$hoverStyle.background};
+ color: ${(p) => p.$hoverStyle.text};
+ border: 1px solid ${(p) => p.$hoverStyle.border};
+ }
+ .drawer-item.active {
+ background-color: ${(p) => p.$activeStyle.background};
+ color: ${(p) => p.$activeStyle.text};
+ border: 1px solid ${(p) => p.$activeStyle.border};
+ }
+`;
+
const TabBarWrapper = styled.div<{
$readOnly: boolean,
$canvasBg: string,
@@ -116,7 +253,7 @@ const StyledTabBar = styled(TabBar)<{
.adm-tab-bar-item-icon, .adm-tab-bar-item-title {
color: ${(props) => props.$tabStyle.text};
}
- .adm-tab-bar-item-icon, {
+ .adm-tab-bar-item-icon {
font-size: ${(props) => props.$navIconSize};
}
@@ -287,6 +424,73 @@ const TabOptionComp = (function () {
.build();
})();
+function renderDataSection(children: any): any {
+ return (
+
+ {children.dataOptionType.propertyView({
+ radioButton: true,
+ type: "oneline",
+ })}
+ {children.dataOptionType.getView() === DataOption.Manual
+ ? children.tabs.propertyView({})
+ : children.jsonItems.propertyView({
+ label: "Json Data",
+ })}
+
+ );
+}
+
+function renderEventHandlersSection(children: any): any {
+ return (
+
+ {children.onEvent.getPropertyView()}
+
+ );
+}
+
+function renderHamburgerLayoutSection(children: any): any {
+ const drawerPlacement = children.drawerPlacement.getView();
+ return (
+ <>
+ {children.hamburgerIcon.propertyView({ label: "Menu Icon" })}
+ {children.drawerCloseIcon.propertyView({ label: "Close Icon" })}
+ {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })}
+ {children.hamburgerSize.propertyView({ label: "Hamburger Size" })}
+ {children.drawerPlacement.propertyView({ label: "Drawer Placement" })}
+ {(drawerPlacement === 'top' || drawerPlacement === 'bottom') &&
+ children.drawerHeight.propertyView({ label: "Drawer Height" })}
+ {(drawerPlacement === 'left' || drawerPlacement === 'right') &&
+ children.drawerWidth.propertyView({ label: "Drawer Width" })}
+ {children.shadowOverlay.propertyView({ label: "Shadow Overlay" })}
+ {children.backgroundImage.propertyView({
+ label: `Background Image`,
+ placeholder: 'https://temp.im/350x400',
+ })}
+ >
+ );
+}
+
+function renderVerticalLayoutSection(children: any): any {
+ return (
+ <>
+ {children.backgroundImage.propertyView({
+ label: `Background Image`,
+ placeholder: 'https://temp.im/350x400',
+ })}
+ {children.showSeparator.propertyView({label: trans("navLayout.mobileNavVerticalShowSeparator")})}
+ {children.tabBarHeight.propertyView({label: trans("navLayout.mobileNavBarHeight")})}
+ {children.navIconSize.propertyView({label: trans("navLayout.mobileNavIconSize")})}
+ {children.maxWidth.propertyView({label: trans("navLayout.mobileNavVerticalMaxWidth")})}
+ {children.verticalAlignment.propertyView({
+ label: trans("navLayout.mobileNavVerticalOrientation"),
+ radioButton: true
+ })}
+ >
+ );
+}
+
+
+
let MobileTabLayoutTmp = (function () {
const childrenMap = {
onEvent: eventHandlerControl(EventOptions),
@@ -311,6 +515,16 @@ let MobileTabLayoutTmp = (function () {
jsonTabs: manualOptionsControl(TabOptionComp, {
initOptions: [],
}),
+ // Mode & hamburger/drawer config
+ menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical),
+ hamburgerIcon: IconControl,
+ drawerCloseIcon: IconControl,
+ hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"),
+ hamburgerSize: withDefault(StringControl, "56px"),
+ drawerPlacement: dropdownControl(DrawerPlacementOptions, "right"),
+ drawerHeight: withDefault(StringControl, "60%"),
+ drawerWidth: withDefault(StringControl, "250px"),
+ shadowOverlay: withDefault(BoolCodeControl, true),
backgroundImage: withDefault(StringControl, ""),
tabBarHeight: withDefault(StringControl, "56px"),
navIconSize: withDefault(StringControl, "32px"),
@@ -321,47 +535,38 @@ let MobileTabLayoutTmp = (function () {
navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'),
navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'),
navItemActiveStyle: styleControl(NavLayoutItemActiveStyle, 'navItemActiveStyle'),
+ hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'),
+ drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'),
};
return new MultiCompBuilder(childrenMap, (props, dispatch) => {
return null;
})
.setPropertyViewFn((children) => {
- const [styleSegment, setStyleSegment] = useState('normal')
+ const [styleSegment, setStyleSegment] = useState('normal');
+ const isHamburgerMode = children.menuMode.getView() === MobileMode.Hamburger;
+
return (
-
-
- {children.dataOptionType.propertyView({
- radioButton: true,
- type: "oneline",
- })}
- {
- children.dataOptionType.getView() === DataOption.Manual
- ? children.tabs.propertyView({})
- : children.jsonItems.propertyView({
- label: "Json Data",
- })
- }
-
-
- { children.onEvent.getPropertyView() }
-
+ <>
+ {renderDataSection(children)}
+ {renderEventHandlersSection(children)}
- {children.backgroundImage.propertyView({
- label: `Background Image`,
- placeholder: 'https://temp.im/350x400',
- })}
- { children.showSeparator.propertyView({label: trans("navLayout.mobileNavVerticalShowSeparator")})}
- {children.tabBarHeight.propertyView({label: trans("navLayout.mobileNavBarHeight")})}
- {children.navIconSize.propertyView({label: trans("navLayout.mobileNavIconSize")})}
- {children.maxWidth.propertyView({label: trans("navLayout.mobileNavVerticalMaxWidth")})}
- {children.verticalAlignment.propertyView(
- { label: trans("navLayout.mobileNavVerticalOrientation"),radioButton: true }
- )}
+ {children.menuMode.propertyView({ label: "Mode", radioButton: true })}
+ {isHamburgerMode
+ ? renderHamburgerLayoutSection(children)
+ : renderVerticalLayoutSection(children)}
-
- { children.navStyle.getPropertyView() }
-
-
+ {!isHamburgerMode && (
+
+ {children.navStyle.getPropertyView()}
+
+ )}
+
+ {isHamburgerMode && (
+
+ {children.hamburgerButtonStyle.getPropertyView()}
+
+ )}
+
{controlItem({}, (
setStyleSegment(k as MenuItemStyleOptionValue)}
/>
))}
- {styleSegment === 'normal' && (
- children.navItemStyle.getPropertyView()
- )}
- {styleSegment === 'hover' && (
- children.navItemHoverStyle.getPropertyView()
- )}
- {styleSegment === 'active' && (
- children.navItemActiveStyle.getPropertyView()
- )}
+ {styleSegment === 'normal' && children.navItemStyle.getPropertyView()}
+ {styleSegment === 'hover' && children.navItemHoverStyle.getPropertyView()}
+ {styleSegment === 'active' && children.navItemActiveStyle.getPropertyView()}
-
+ {isHamburgerMode && (
+
+ {children.drawerContainerStyle.getPropertyView()}
+
+ )}
+ >
);
})
.build();
@@ -388,7 +592,9 @@ let MobileTabLayoutTmp = (function () {
MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => {
const [tabIndex, setTabIndex] = useState(0);
+ const [drawerVisible, setDrawerVisible] = useState(false);
const { readOnly } = useContext(ExternalEditorContext);
+ const pathParam = useAppPathParam();
const navStyle = comp.children.navStyle.getView();
const navItemStyle = comp.children.navItemStyle.getView();
const navItemHoverStyle = comp.children.navItemHoverStyle.getView();
@@ -396,14 +602,32 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => {
const backgroundImage = comp.children.backgroundImage.getView();
const jsonItems = comp.children.jsonItems.getView();
const dataOptionType = comp.children.dataOptionType.getView();
+ const menuMode = comp.children.menuMode.getView();
+ const hamburgerPosition = comp.children.hamburgerPosition.getView();
+ const hamburgerSize = comp.children.hamburgerSize.getView();
+ const hamburgerIconComp = comp.children.hamburgerIcon;
+ const drawerCloseIconComp = comp.children.drawerCloseIcon;
+ const hamburgerButtonStyle = comp.children.hamburgerButtonStyle.getView();
+ const drawerPlacement = comp.children.drawerPlacement.getView();
+ const drawerHeight = comp.children.drawerHeight.getView();
+ const drawerWidth = comp.children.drawerWidth.getView();
+ const shadowOverlay = comp.children.shadowOverlay.getView();
const tabBarHeight = comp.children.tabBarHeight.getView();
const navIconSize = comp.children.navIconSize.getView();
const maxWidth = comp.children.maxWidth.getView();
const verticalAlignment = comp.children.verticalAlignment.getView();
const showSeparator = comp.children.showSeparator.getView();
+ const drawerContainerStyle = comp.children.drawerContainerStyle.getView();
const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas;
const onEvent = comp.children.onEvent.getView();
+ const getContainer = useCallback(() =>
+ document.querySelector(`#${PreviewContainerID}`) ||
+ document.querySelector(`#${CanvasContainerID}`) ||
+ document.body,
+ []
+ );
+
useEffect(() => {
comp.children.jsonTabs.dispatchChangeValueAction({
manual: jsonItems as unknown as Array>
@@ -455,6 +679,21 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => {
backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`;
}
+ const navigateToApp = (nextIndex: number) => {
+ if (dataOptionType === DataOption.Manual) {
+ const selectedTab = tabViews[nextIndex];
+ if (selectedTab) {
+ const url = [
+ ALL_APPLICATIONS_URL,
+ pathParam.applicationId,
+ pathParam.viewMode,
+ nextIndex,
+ ].join("/");
+ selectedTab.children.action.act(url);
+ }
+ }
+ };
+
const tabBarView = (
{
: undefined,
}))}
selectedKey={tabIndex + ""}
- onChange={(key) => setTabIndex(Number(key))}
+ onChange={(key) => {
+ const nextIndex = Number(key);
+ setTabIndex(nextIndex);
+ // push URL with query/hash params
+ navigateToApp(nextIndex);
+ }}
readOnly={!!readOnly}
canvasBg={bgColor}
tabStyle={{
@@ -488,11 +732,111 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => {
/>
);
+ const containerTabBarHeight = menuMode === MobileMode.Hamburger ? '0px' : tabBarHeight;
+
+ const hamburgerButton = (
+ setDrawerVisible(true)}
+ >
+ {hamburgerIconComp.toJsonValue() ? (
+
+ {hamburgerIconComp.getView()}
+
+ ) : (
+
+ )}
+
+ );
+
+ const drawerView = (
+ }>
+ setDrawerVisible(false)}
+ placement={drawerPlacement as any}
+ mask={shadowOverlay}
+ maskClosable={true}
+ closable={false}
+ styles={{ body: { padding: 0 } } as any}
+ getContainer={getContainer}
+ width={
+ (drawerPlacement === 'left' || drawerPlacement === 'right')
+ ? (drawerWidth as any)
+ : undefined
+ }
+ height={
+ (drawerPlacement === 'top' || drawerPlacement === 'bottom')
+ ? (drawerHeight as any)
+ : undefined
+ }
+ >
+
+
+ setDrawerVisible(false)}
+ >
+ {drawerCloseIconComp.toJsonValue()
+ ? drawerCloseIconComp.getView()
+ : ×}
+
+
+
+ {tabViews.map((tab, index) => (
+ {
+ setTabIndex(index);
+ setDrawerVisible(false);
+ onEvent('click');
+ navigateToApp(index);
+ }}
+ >
+ {tab.children.icon.toJsonValue() ? (
+ {tab.children.icon.getView()}
+ ) : null}
+ {tab.children.label.getView()}
+
+ ))}
+
+
+
+
+ );
+
if (readOnly) {
return (
-
+
{appView}
- {tabBarView}
+ {menuMode === MobileMode.Hamburger ? (
+ <>
+ {hamburgerButton}
+ {drawerView}
+ >
+ ) : (
+ tabBarView
+ )}
);
}
@@ -500,7 +844,14 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => {
return (
{appView}
- {tabBarView}
+ {menuMode === MobileMode.Hamburger ? (
+ <>
+ {hamburgerButton}
+ {drawerView}
+ >
+ ) : (
+ tabBarView
+ )}
);
});
diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts
index 66043303ac..aa33423d02 100644
--- a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts
+++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts
@@ -6,6 +6,45 @@ export const ModeOptions = [
{ label: trans("navLayout.modeHorizontal"), value: "horizontal" },
] as const;
+// Mobile navigation specific modes and options
+export const MobileMode = {
+ Vertical: "vertical",
+ Hamburger: "hamburger",
+} as const;
+
+export const MobileModeOptions = [
+ { label: "Normal", value: MobileMode.Vertical },
+ { label: "Hamburger", value: MobileMode.Hamburger },
+];
+
+export const HamburgerPosition = {
+ BottomRight: "bottom-right",
+ BottomLeft: "bottom-left",
+ TopRight: "top-right",
+ TopLeft: "top-left",
+} as const;
+
+export const HamburgerPositionOptions = [
+ { label: "Bottom Right", value: HamburgerPosition.BottomRight },
+ { label: "Bottom Left", value: HamburgerPosition.BottomLeft },
+ { label: "Top Right", value: HamburgerPosition.TopRight },
+ { label: "Top Left", value: HamburgerPosition.TopLeft },
+] as const;
+
+export const DrawerPlacement = {
+ Bottom: "bottom",
+ Top: "top",
+ Left: "left",
+ Right: "right",
+} as const;
+
+export const DrawerPlacementOptions = [
+ { label: "Bottom", value: DrawerPlacement.Bottom },
+ { label: "Top", value: DrawerPlacement.Top },
+ { label: "Left", value: DrawerPlacement.Left },
+ { label: "Right", value: DrawerPlacement.Right },
+];
+
export const DataOption = {
Manual: 'manual',
Json: 'json',
diff --git a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx
index 670f4bba91..27ea116772 100644
--- a/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/navComp/navComp.tsx
@@ -5,26 +5,34 @@ import { Section, sectionNames } from "lowcoder-design";
import styled from "styled-components";
import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl";
import { BoolCodeControl, StringControl } from "comps/controls/codeControl";
+import { dropdownControl } from "comps/controls/dropdownControl";
import { alignWithJustifyControl } from "comps/controls/alignControl";
import { navListComp } from "./navItemComp";
import { menuPropertyView } from "./components/MenuItemList";
import { default as DownOutlined } from "@ant-design/icons/DownOutlined";
+import { default as MenuOutlined } from "@ant-design/icons/MenuOutlined";
import { default as Dropdown } from "antd/es/dropdown";
import { default as Menu, MenuProps } from "antd/es/menu";
+import { default as Drawer } from "antd/es/drawer";
import { migrateOldData } from "comps/generators/simpleGenerators";
import { styleControl } from "comps/controls/styleControl";
import {
AnimationStyle,
AnimationStyleType,
NavigationStyle,
+ HamburgerButtonStyle,
+ DrawerContainerStyle,
+ NavLayoutItemStyle,
+ NavLayoutItemHoverStyle,
+ NavLayoutItemActiveStyle,
} from "comps/controls/styleControlConstants";
import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils";
import { trans } from "i18n";
-import { useContext } from "react";
+import { useContext, useState } from "react";
import { EditorContext } from "comps/editorState";
-import { controlItem } from "lowcoder-design";
import { createNavItemsControl } from "./components/NavItemsControl";
+import { Layers } from "constants/Layers";
type IProps = {
$justify: boolean;
@@ -34,6 +42,7 @@ type IProps = {
$borderRadius: string;
$borderStyle: string;
$animationStyle: AnimationStyleType;
+ $orientation: "horizontal" | "vertical";
};
const Wrapper = styled("div")<
@@ -45,18 +54,21 @@ ${props=>props.$animationStyle}
box-sizing: border-box;
border: ${(props) => props.$borderWidth ? `${props.$borderWidth}` : '1px'} ${props=>props.$borderStyle} ${(props) => props.$borderColor};
background: ${(props) => props.$bgColor};
+ position: relative;
`;
-const NavInner = styled("div") >`
+const NavInner = styled("div") >`
// margin: 0 -16px;
height: 100%;
display: flex;
- justify-content: ${(props) => (props.$justify ? "space-between" : "left")};
+ flex-direction: ${(props) => (props.$orientation === "vertical" ? "column" : "row")};
+ justify-content: ${(props) => (props.$orientation === "vertical" ? "flex-start" : (props.$justify ? "space-between" : "left"))};
`;
const Item = styled.div<{
$active: boolean;
$activeColor: string;
+ $hoverColor: string;
$color: string;
$fontFamily: string;
$fontStyle: string;
@@ -66,12 +78,22 @@ const Item = styled.div<{
$padding: string;
$textTransform:string;
$textDecoration:string;
+ $bg?: string;
+ $hoverBg?: string;
+ $activeBg?: string;
+ $border?: string;
+ $hoverBorder?: string;
+ $activeBorder?: string;
+ $radius?: string;
$disabled?: boolean;
}>`
height: 30px;
line-height: 30px;
padding: ${(props) => props.$padding ? props.$padding : '0 16px'};
color: ${(props) => props.$disabled ? `${props.$color}80` : (props.$active ? props.$activeColor : props.$color)};
+ background-color: ${(props) => (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent'))};
+ border: ${(props) => props.$border ? `1px solid ${props.$border}` : '1px solid transparent'};
+ border-radius: ${(props) => props.$radius ? props.$radius : '0px'};
font-weight: ${(props) => (props.$textWeight ? props.$textWeight : 500)};
font-family:${(props) => (props.$fontFamily ? props.$fontFamily : 'sans-serif')};
font-style:${(props) => (props.$fontStyle ? props.$fontStyle : 'normal')};
@@ -81,7 +103,9 @@ const Item = styled.div<{
margin:${(props) => props.$margin ? props.$margin : '0px'};
&:hover {
- color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : props.$activeColor};
+ color: ${(props) => props.$disabled ? (props.$active ? props.$activeColor : props.$color) : (props.$hoverColor || props.$activeColor)};
+ background-color: ${(props) => props.$disabled ? (props.$active ? (props.$activeBg || 'transparent') : (props.$bg || 'transparent')) : (props.$hoverBg || props.$activeBg || props.$bg || 'transparent')};
+ border: ${(props) => props.$hoverBorder ? `1px solid ${props.$hoverBorder}` : (props.$activeBorder ? `1px solid ${props.$activeBorder}` : (props.$border ? `1px solid ${props.$border}` : '1px solid transparent'))};
cursor: ${(props) => props.$disabled ? 'not-allowed' : 'pointer'};
}
@@ -101,10 +125,10 @@ const LogoWrapper = styled.div`
}
`;
-const ItemList = styled.div<{ $align: string }>`
+const ItemList = styled.div<{ $align: string, $orientation?: string }>`
flex: 1;
display: flex;
- flex-direction: row;
+ flex-direction: ${(props) => (props.$orientation === "vertical" ? "column" : "row")};
justify-content: ${(props) => props.$align};
`;
@@ -114,6 +138,37 @@ const StyledMenu = styled(Menu) `
}
`;
+const FloatingHamburgerButton = styled.button<{
+ $size: string;
+ $position: string; // top-right | top-left | bottom-right | bottom-left
+ $zIndex: number;
+ $background?: string;
+ $borderColor?: string;
+ $radius?: string;
+ $margin?: string;
+ $padding?: string;
+ $borderWidth?: string;
+ $iconColor?: string;
+}>`
+ position: fixed;
+ ${(props) => (props.$position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;')}
+ ${(props) => (props.$position.includes('right') ? 'right: 16px;' : 'left: 16px;')}
+ width: ${(props) => props.$size};
+ height: ${(props) => props.$size};
+ border-radius: ${(props) => props.$radius || '50%'};
+ border: ${(props) => props.$borderWidth || '1px'} solid ${(props) => props.$borderColor || 'rgba(0,0,0,0.1)'};
+ background: ${(props) => props.$background || 'white'};
+ margin: ${(props) => props.$margin || '0px'};
+ padding: ${(props) => props.$padding || '0px'};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: ${(props) => props.$zIndex};
+ cursor: pointer;
+ box-shadow: 0 6px 16px rgba(0,0,0,0.15);
+ color: ${(props) => props.$iconColor || 'inherit'};
+`;
+
const logoEventHandlers = [clickEvent];
// Compatible with historical style data 2022-8-26
@@ -154,8 +209,33 @@ function fixOldItemsData(oldData: any) {
const childrenMap = {
logoUrl: StringControl,
logoEvent: withDefault(eventHandlerControl(logoEventHandlers), [{ name: "click" }]),
+ orientation: dropdownControl([
+ { label: "Horizontal", value: "horizontal" },
+ { label: "Vertical", value: "vertical" },
+ ], "horizontal"),
+ displayMode: dropdownControl([
+ { label: "Bar", value: "bar" },
+ { label: "Hamburger", value: "hamburger" },
+ ], "bar"),
+ hamburgerPosition: dropdownControl([
+ { label: "Top Right", value: "top-right" },
+ { label: "Top Left", value: "top-left" },
+ { label: "Bottom Right", value: "bottom-right" },
+ { label: "Bottom Left", value: "bottom-left" },
+ ], "top-right"),
+ hamburgerSize: withDefault(StringControl, "56px"),
+ drawerPlacement: dropdownControl([
+ { label: "Left", value: "left" },
+ { label: "Right", value: "right" },
+ ], "right"),
+ shadowOverlay: withDefault(BoolCodeControl, true),
horizontalAlignment: alignWithJustifyControl(),
style: migrateOldData(styleControl(NavigationStyle, 'style'), fixOldStyleData),
+ navItemStyle: styleControl(NavLayoutItemStyle, 'navItemStyle'),
+ navItemHoverStyle: styleControl(NavLayoutItemHoverStyle, 'navItemHoverStyle'),
+ navItemActiveStyle: styleControl(NavLayoutItemActiveStyle, 'navItemActiveStyle'),
+ hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'),
+ drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'),
animationStyle: styleControl(AnimationStyle, 'animationStyle'),
items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), {
optionType: "manual",
@@ -168,6 +248,7 @@ const childrenMap = {
};
const NavCompBase = new UICompBuilder(childrenMap, (props) => {
+ const [drawerVisible, setDrawerVisible] = useState(false);
const data = props.items;
const items = (
<>
@@ -207,16 +288,24 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
- 0}
- $color={props.style.text}
- $activeColor={props.style.accent}
+ $color={(props.navItemStyle && props.navItemStyle.text) || props.style.text}
+ $hoverColor={(props.navItemHoverStyle && props.navItemHoverStyle.text) || props.style.accent}
+ $activeColor={(props.navItemActiveStyle && props.navItemActiveStyle.text) || props.style.accent}
$fontFamily={props.style.fontFamily}
$fontStyle={props.style.fontStyle}
$textWeight={props.style.textWeight}
$textSize={props.style.textSize}
- $padding={props.style.padding}
+ $padding={(props.navItemStyle && props.navItemStyle.padding) || props.style.padding}
$textTransform={props.style.textTransform}
$textDecoration={props.style.textDecoration}
- $margin={props.style.margin}
+ $margin={(props.navItemStyle && props.navItemStyle.margin) || props.style.margin}
+ $bg={(props.navItemStyle && props.navItemStyle.background) || undefined}
+ $hoverBg={(props.navItemHoverStyle && props.navItemHoverStyle.background) || undefined}
+ $activeBg={(props.navItemActiveStyle && props.navItemActiveStyle.background) || undefined}
+ $border={(props.navItemStyle && props.navItemStyle.border) || undefined}
+ $hoverBorder={(props.navItemHoverStyle && props.navItemHoverStyle.border) || undefined}
+ $activeBorder={(props.navItemActiveStyle && props.navItemActiveStyle.border) || undefined}
+ $radius={(props.navItemStyle && props.navItemStyle.radius) || undefined}
$disabled={disabled}
onClick={() => { if (!disabled && onEvent) onEvent("click"); }}
>
@@ -255,6 +344,8 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
);
const justify = props.horizontalAlignment === "justify";
+ const isVertical = props.orientation === "vertical";
+ const isHamburger = props.displayMode === "hamburger";
return (
{
$borderWidth={props.style.borderWidth}
$borderRadius={props.style.radius}
>
-
- {props.logoUrl && (
- props.logoEvent("click")}>
-
-
- )}
- {!justify ? {items} : items}
-
+ {!isHamburger && (
+
+ {props.logoUrl && (
+ props.logoEvent("click")}>
+
+
+ )}
+ {!justify ? {items} : items}
+
+ )}
+ {isHamburger && (
+ <>
+ setDrawerVisible(true)}
+ >
+
+
+ setDrawerVisible(false)}
+ open={drawerVisible}
+ mask={props.shadowOverlay}
+ styles={{ body: { padding: "8px", background: props.drawerContainerStyle?.background } }}
+ destroyOnClose
+ >
+ {items}
+
+ >
+ )}
);
})
@@ -292,10 +415,21 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
{(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && (
- {children.horizontalAlignment.propertyView({
- label: trans("navigation.horizontalAlignment"),
- radioButton: true,
- })}
+ {children.orientation.propertyView({ label: "Orientation", radioButton: true })}
+ {children.displayMode.propertyView({ label: "Display Mode", radioButton: true })}
+ {children.displayMode.getView() === 'hamburger' ? (
+ [
+ children.hamburgerPosition.propertyView({ label: "Hamburger Position" }),
+ children.hamburgerSize.propertyView({ label: "Hamburger Size" }),
+ children.drawerPlacement.propertyView({ label: "Drawer Placement", radioButton: true }),
+ children.shadowOverlay.propertyView({ label: "Shadow Overlay" }),
+ ]
+ ) : (
+ children.horizontalAlignment.propertyView({
+ label: trans("navigation.horizontalAlignment"),
+ radioButton: true,
+ })
+ )}
{hiddenPropertyView(children)}
)}
@@ -313,6 +447,25 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
{children.style.getPropertyView()}
+
+ {children.navItemStyle.getPropertyView()}
+
+
+ {children.navItemHoverStyle.getPropertyView()}
+
+
+ {children.navItemActiveStyle.getPropertyView()}
+
+ {children.displayMode.getView() === 'hamburger' && (
+ <>
+
+ {children.hamburgerButtonStyle.getPropertyView()}
+
+
+ {children.drawerContainerStyle.getPropertyView()}
+
+ >
+ )}
{children.animationStyle.getPropertyView()}
diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
index 569ada9c4d..175448bf3b 100644
--- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
+++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
@@ -1382,6 +1382,30 @@ export const FloatButtonStyle = [
BORDER_WIDTH,
] as const;
+export const HamburgerButtonStyle = [
+ getBackground(),
+ {
+ name: "iconFill",
+ label: trans("style.fill"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ MARGIN,
+ PADDING,
+ BORDER,
+ RADIUS,
+ BORDER_WIDTH,
+] as const;
+
+export const DrawerContainerStyle = [
+ getBackground(),
+ MARGIN,
+ PADDING,
+ BORDER,
+ BORDER_WIDTH,
+] as const;
+
export const TransferStyle = [
getStaticBackground(SURFACE_COLOR),
...STYLING_FIELDS_CONTAINER_SEQUENCE.filter(style=>style.name!=='rotation'),
diff --git a/client/packages/lowcoder/src/constants/domLocators.ts b/client/packages/lowcoder/src/constants/domLocators.ts
index b3d1709a5b..2fefcb5f1f 100644
--- a/client/packages/lowcoder/src/constants/domLocators.ts
+++ b/client/packages/lowcoder/src/constants/domLocators.ts
@@ -1,2 +1,3 @@
export const CanvasContainerID = "__canvas_container__";
export const CodeEditorTooltipContainerID = "__code_editor_tooltip__";
+export const PreviewContainerID = "__preview_container__";
diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx
index c722f907f7..a60f9f0ef3 100644
--- a/client/packages/lowcoder/src/pages/editor/editorView.tsx
+++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx
@@ -64,6 +64,7 @@ import { isEqual, noop } from "lodash";
import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext";
import { getBrandingSetting } from "@lowcoder-ee/redux/selectors/enterpriseSelectors";
import Flex from "antd/es/flex";
+import { PreviewContainerID } from "constants/domLocators";
// import { BottomSkeleton } from "./bottom/BottomContent";
const Header = lazy(
@@ -270,6 +271,7 @@ const DeviceWrapperInner = styled(Flex)`
> div:first-child {
> div:first-child {
> div:nth-child(2) {
+ contain: paint;
display: block !important;
overflow: hidden auto !important;
}
@@ -533,10 +535,12 @@ function EditorView(props: EditorViewProps) {
deviceType={editorState.deviceType}
deviceOrientation={editorState.deviceOrientation}
>
- {uiComp.getView()}
+
+ {uiComp.getView()}
+
) : (
-
+
{uiComp.getView()}
)
diff --git a/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx b/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx
index f5e90bd7b2..f051a28983 100644
--- a/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx
+++ b/client/packages/lowcoder/src/pages/editor/right/PropertyView.tsx
@@ -26,7 +26,7 @@ export default function PropertyView(props: PropertyViewProps) {
let propertyView;
if (selectedComp) {
- return <>{selectedComp.getPropertyView()}>;
+ propertyView = selectedComp.getPropertyView();
} else if (selectedCompNames.size > 1) {
propertyView = (