From f4109c652bd621d4c5e63e588109972212aa1d2e Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 19:36:12 +0500 Subject: [PATCH 1/6] [Fix]: #2076 mobile nav app not passing params --- .../comps/comps/layout/mobileTabLayout.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index c1a04c14ea..d5c0522697 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -34,6 +34,8 @@ 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(() => @@ -389,6 +391,7 @@ let MobileTabLayoutTmp = (function () { MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const [tabIndex, setTabIndex] = useState(0); 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(); @@ -466,7 +469,23 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { : undefined, }))} selectedKey={tabIndex + ""} - onChange={(key) => setTabIndex(Number(key))} + onChange={(key) => { + const nextIndex = Number(key); + setTabIndex(nextIndex); + // push URL with query/hash params like desktop nav + 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); + } + } + }} readOnly={!!readOnly} canvasBg={bgColor} tabStyle={{ From 16e9d0a3881986646be2958b64681668bf917cc0 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 22:30:04 +0500 Subject: [PATCH 2/6] [Feat]: Add hamburger menu mode for Mobile Nav App + refactor --- .../comps/comps/layout/mobileTabLayout.tsx | 305 +++++++++++++++--- .../comps/comps/layout/navLayoutConstants.ts | 39 +++ 2 files changed, 298 insertions(+), 46 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index d5c0522697..a283c5f393 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -18,7 +18,7 @@ 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 Segmented from "antd/es/segmented"; @@ -43,6 +43,7 @@ const TabBarItem = React.lazy(() => default: module.TabBarItem, })) ); +const Popup = React.lazy(() => import("antd-mobile/es/components/popup")); const EventOptions = [clickEvent] as const; const AppViewContainer = styled.div` @@ -67,6 +68,92 @@ 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; +}>` + 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: 50%; + border: 1px solid rgba(0,0,0,0.1); + background: white; + 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 DrawerContent = styled.div<{ + $background: string; +}>` + background: ${(p) => p.$background}; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 12px; + box-sizing: border-box; +`; + +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, @@ -118,7 +205,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}; } @@ -289,6 +376,69 @@ 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.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), @@ -313,6 +463,14 @@ let MobileTabLayoutTmp = (function () { jsonTabs: manualOptionsControl(TabOptionComp, { initOptions: [], }), + // Mode & hamburger/drawer config + menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical), + hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), + hamburgerSize: withDefault(StringControl, "56px"), + drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), + drawerHeight: withDefault(StringControl, "60%"), + drawerWidth: withDefault(StringControl, "250px"), + shadowOverlay: withDefault(BoolCodeControl, true), backgroundImage: withDefault(StringControl, ""), tabBarHeight: withDefault(StringControl, "56px"), navIconSize: withDefault(StringControl, "32px"), @@ -328,40 +486,21 @@ let MobileTabLayoutTmp = (function () { 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() } + {children.navStyle.getPropertyView()}
{controlItem({}, ( @@ -372,17 +511,11 @@ let MobileTabLayoutTmp = (function () { onChange={(k) => 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()}
-
+ ); }) .build(); @@ -390,6 +523,7 @@ 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(); @@ -399,6 +533,13 @@ 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 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(); @@ -472,7 +613,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { onChange={(key) => { const nextIndex = Number(key); setTabIndex(nextIndex); - // push URL with query/hash params like desktop nav + // push URL with query/hash params if (dataOptionType === DataOption.Manual) { const selectedTab = tabViews[nextIndex]; if (selectedTab) { @@ -507,11 +648,76 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { /> ); + const containerTabBarHeight = menuMode === MobileMode.Hamburger ? '0px' : tabBarHeight; + + const hamburgerButton = ( + setDrawerVisible(true)} + > + + + ); + + const drawerBodyStyle = useMemo(() => { + if (drawerPlacement === 'left' || drawerPlacement === 'right') { + return { width: drawerWidth } as React.CSSProperties; + } + return { height: drawerHeight } as React.CSSProperties; + }, [drawerPlacement, drawerHeight, drawerWidth]); + + const drawerView = ( + }> + setDrawerVisible(false)} + onClose={() => setDrawerVisible(false)} + position={drawerPlacement as any} + mask={shadowOverlay} + bodyStyle={drawerBodyStyle} + > + + + {tabViews.map((tab, index) => ( +
{ + setTabIndex(index); + setDrawerVisible(false); + onEvent('click'); + }} + > + {tab.children.icon.toJsonValue() ? ( + {tab.children.icon.getView()} + ) : null} + {tab.children.label.getView()} +
+ ))} +
+
+
+
+ ); + if (readOnly) { return ( - + {appView} - {tabBarView} + {menuMode === MobileMode.Hamburger ? ( + <> + {hamburgerButton} + {drawerView} + + ) : ( + tabBarView + )} ); } @@ -519,7 +725,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', From 9408350f87339f2a67900096ce50f421f4f9d744 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 4 Nov 2025 22:35:23 +0500 Subject: [PATCH 3/6] add customizeable icon burger icon --- .../lowcoder/src/comps/comps/layout/mobileTabLayout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index a283c5f393..a019c97eff 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -404,6 +404,7 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> + {children.hamburgerIcon.propertyView({ label: "Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} {children.drawerPlacement.propertyView({ label: "Drawer Placement" })} @@ -465,6 +466,7 @@ let MobileTabLayoutTmp = (function () { }), // Mode & hamburger/drawer config menuMode: dropdownControl(MobileModeOptions, MobileMode.Vertical), + hamburgerIcon: IconControl, hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), hamburgerSize: withDefault(StringControl, "56px"), drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), @@ -536,6 +538,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const menuMode = comp.children.menuMode.getView(); const hamburgerPosition = comp.children.hamburgerPosition.getView(); const hamburgerSize = comp.children.hamburgerSize.getView(); + const hamburgerIconComp = comp.children.hamburgerIcon; const drawerPlacement = comp.children.drawerPlacement.getView(); const drawerHeight = comp.children.drawerHeight.getView(); const drawerWidth = comp.children.drawerWidth.getView(); @@ -657,7 +660,9 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { $zIndex={Layers.tabBar + 1} onClick={() => setDrawerVisible(true)} > - + {hamburgerIconComp.toJsonValue() + ? hamburgerIconComp.getView() + : } ); From b15b10ac5fed17d489c9dc4e3beefc8d399e7d02 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 5 Nov 2025 15:03:09 +0500 Subject: [PATCH 4/6] add close icon in the drawer --- .../comps/comps/layout/mobileTabLayout.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index a019c97eff..1d498be4f9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -120,6 +120,27 @@ const DrawerContent = styled.div<{ box-sizing: border-box; `; +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; @@ -404,7 +425,8 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> - {children.hamburgerIcon.propertyView({ label: "Icon" })} + {children.hamburgerIcon.propertyView({ label: "MenuIcon" })} + {children.drawerCloseIcon.propertyView({ label: "Close Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} {children.drawerPlacement.propertyView({ label: "Drawer Placement" })} @@ -467,6 +489,7 @@ let MobileTabLayoutTmp = (function () { // 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, "bottom"), @@ -539,6 +562,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const hamburgerPosition = comp.children.hamburgerPosition.getView(); const hamburgerSize = comp.children.hamburgerSize.getView(); const hamburgerIconComp = comp.children.hamburgerIcon; + const drawerCloseIconComp = comp.children.drawerCloseIcon; const drawerPlacement = comp.children.drawerPlacement.getView(); const drawerHeight = comp.children.drawerHeight.getView(); const drawerWidth = comp.children.drawerWidth.getView(); @@ -684,6 +708,17 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { bodyStyle={drawerBodyStyle} > + + setDrawerVisible(false)} + > + {drawerCloseIconComp.toJsonValue() + ? drawerCloseIconComp.getView() + : ×} + + Date: Wed, 5 Nov 2025 19:37:27 +0500 Subject: [PATCH 5/6] add style customization for hamburger menu mode --- .../comps/comps/layout/mobileTabLayout.tsx | 120 ++++++++++++++---- .../comps/controls/styleControlConstants.tsx | 24 ++++ 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 1d498be4f9..5073dddc9a 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -20,7 +20,7 @@ import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; 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"; @@ -72,15 +72,23 @@ 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: 50%; - border: 1px solid rgba(0,0,0,0.1); - background: white; + 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; @@ -108,16 +116,34 @@ const BurgerIcon = styled.div<{ &::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: 12px; + 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` @@ -425,7 +451,7 @@ function renderHamburgerLayoutSection(children: any): any { const drawerPlacement = children.drawerPlacement.getView(); return ( <> - {children.hamburgerIcon.propertyView({ label: "MenuIcon" })} + {children.hamburgerIcon.propertyView({ label: "Menu Icon" })} {children.drawerCloseIcon.propertyView({ label: "Close Icon" })} {children.hamburgerPosition.propertyView({ label: "Hamburger Position" })} {children.hamburgerSize.propertyView({ label: "Hamburger Size" })} @@ -462,6 +488,8 @@ function renderVerticalLayoutSection(children: any): any { ); } + + let MobileTabLayoutTmp = (function () { const childrenMap = { onEvent: eventHandlerControl(EventOptions), @@ -492,7 +520,7 @@ let MobileTabLayoutTmp = (function () { drawerCloseIcon: IconControl, hamburgerPosition: dropdownControl(HamburgerPositionOptions, "bottom-right"), hamburgerSize: withDefault(StringControl, "56px"), - drawerPlacement: dropdownControl(DrawerPlacementOptions, "bottom"), + drawerPlacement: dropdownControl(DrawerPlacementOptions, "right"), drawerHeight: withDefault(StringControl, "60%"), drawerWidth: withDefault(StringControl, "250px"), shadowOverlay: withDefault(BoolCodeControl, true), @@ -506,6 +534,8 @@ 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; @@ -524,10 +554,18 @@ let MobileTabLayoutTmp = (function () { ? renderHamburgerLayoutSection(children) : renderVerticalLayoutSection(children)} -
- {children.navStyle.getPropertyView()} -
-
+ {!isHamburgerMode && ( +
+ {children.navStyle.getPropertyView()} +
+ )} + + {isHamburgerMode && ( +
+ {children.hamburgerButtonStyle.getPropertyView()} +
+ )} +
{controlItem({}, ( + {isHamburgerMode && ( +
+ {children.drawerContainerStyle.getPropertyView()} +
+ )} ); }) @@ -563,6 +606,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { 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(); @@ -572,6 +616,7 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { 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(); @@ -626,6 +671,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 = ( { const nextIndex = Number(key); setTabIndex(nextIndex); // push URL with query/hash params - 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); - } - } + navigateToApp(nextIndex); }} readOnly={!!readOnly} canvasBg={bgColor} @@ -682,11 +731,21 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { $size={hamburgerSize} $position={hamburgerPosition} $zIndex={Layers.tabBar + 1} + $background={hamburgerButtonStyle?.background} + $borderColor={hamburgerButtonStyle?.border} + $radius={hamburgerButtonStyle?.radius} + $margin={hamburgerButtonStyle?.margin} + $padding={hamburgerButtonStyle?.padding} + $borderWidth={hamburgerButtonStyle?.borderWidth} onClick={() => setDrawerVisible(true)} > - {hamburgerIconComp.toJsonValue() - ? hamburgerIconComp.getView() - : } + {hamburgerIconComp.toJsonValue() ? ( + + {hamburgerIconComp.getView()} + + ) : ( + + )} ); @@ -707,7 +766,13 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { mask={shadowOverlay} bodyStyle={drawerBodyStyle} > - + { setTabIndex(index); setDrawerVisible(false); onEvent('click'); + navigateToApp(index); }} > {tab.children.icon.toJsonValue() ? ( 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'), From b2254a0656ce7808962820295fa56ee9ddfbe63c Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 5 Nov 2025 22:40:14 +0500 Subject: [PATCH 6/6] fix scroll issue for the nav apps propertyview --- .../packages/lowcoder/src/pages/editor/right/PropertyView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = (