diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx index 62b55a7da..0999a4012 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx @@ -38,15 +38,13 @@ export class LayoutMenuItemComp extends MultiBaseComp<ChildrenType> { } override getPropertyView(): ReactNode { - const isLeaf = this.children.items.getView().length === 0; return ( <> - {isLeaf && - this.children.action.propertyView({ - onAppChange: (label) => { - label && this.children.label.dispatchChangeValueAction(label); - }, - })} + {this.children.action.propertyView({ + onAppChange: (label) => { + label && this.children.label.dispatchChangeValueAction(label); + }, + })} {this.children.label.propertyView({ label: trans("label") })} {this.children.icon.propertyView({ label: trans("icon"), @@ -98,12 +96,17 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) { addItem(value?: any) { const data = this.getView(); + this.dispatch( this.pushAction( - value || { - label: trans("menuItem") + " " + (data.length + 1), - itemKey: genRandomKey(), - } + value + ? { + ...value, + itemKey: value.itemKey || genRandomKey(), + } : { + label: trans("menuItem") + " " + (data.length + 1), + itemKey: genRandomKey(), + } ) ); } diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 5e8d47320..368e459a9 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -1,4 +1,4 @@ -import { Layout, Menu as AntdMenu, MenuProps } from "antd"; +import { Layout, Menu as AntdMenu, MenuProps, Segmented } from "antd"; import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; @@ -8,12 +8,38 @@ import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; import { ALL_APPLICATIONS_URL } from "constants/routesURL"; import { TopHeaderHeight } from "constants/style"; -import { Section } from "lowcoder-design"; +import { Section, controlItem, sectionNames } from "lowcoder-design"; import { trans } from "i18n"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { useCallback, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; +import { StringControl, jsonControl } from "comps/controls/codeControl"; +import { styleControl } from "comps/controls/styleControl"; +import { + NavLayoutStyle, + NavLayoutItemStyle, + NavLayoutItemStyleType, + NavLayoutItemHoverStyle, + NavLayoutItemHoverStyleType, + NavLayoutItemActiveStyle, + NavLayoutItemActiveStyleType, +} from "comps/controls/styleControlConstants"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import _ from "lodash"; +import { check } from "util/convertUtils"; +import { genRandomKey } from "comps/utils/idGenerator"; +import history from "util/history"; +import { + DataOption, + DataOptionType, + ModeOptions, + jsonMenuItems, + menuItemStyleOptions +} from "./navLayoutConstants"; + +const DEFAULT_WIDTH = 240; +type MenuItemStyleOptionValue = "normal" | "hover" | "active"; const StyledSide = styled(Layout.Sider)` max-height: calc(100vh - ${TopHeaderHeight}); @@ -39,22 +65,192 @@ const ContentWrapper = styled.div` } `; +const StyledMenu = styled(AntdMenu)<{ + $navItemStyle?: NavLayoutItemStyleType & { width: string}, + $navItemHoverStyle?: NavLayoutItemHoverStyleType, + $navItemActiveStyle?: NavLayoutItemActiveStyleType, +}>` + .ant-menu-item { + height: auto; + width: ${(props) => props.$navItemStyle?.width}; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: ${(props) => props.$navItemStyle?.margin}; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + .ant-menu-item-active { + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + + .ant-menu-item-selected { + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + + .ant-menu-submenu { + margin: ${(props) => props.$navItemStyle?.margin}; + width: ${(props) => props.$navItemStyle?.width}; + + .ant-menu-submenu-title { + width: 100%; + height: auto !important; + background-color: ${(props) => props.$navItemStyle?.background}; + color: ${(props) => props.$navItemStyle?.text}; + border-radius: ${(props) => props.$navItemStyle?.radius} !important; + border: ${(props) => `1px solid ${props.$navItemStyle?.border}`}; + margin: 0; + padding: ${(props) => props.$navItemStyle?.padding}; + + } + + .ant-menu-item { + width: 100%; + } + + &.ant-menu-submenu-active { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemHoverStyle?.background} !important; + color: ${(props) => props.$navItemHoverStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`}; + } + } + &.ant-menu-submenu-selected { + >.ant-menu-submenu-title { + width: 100%; + background-color: ${(props) => props.$navItemActiveStyle?.background} !important; + color: ${(props) => props.$navItemActiveStyle?.text} !important; + border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`}; + } + } + } + +`; + +const StyledImage = styled.img` + height: 1em; + color: currentColor; +`; + +const defaultStyle = { + radius: '0px', + margin: '0px', + padding: '0px', +} + +type UrlActionType = { + url?: string; + newTab?: boolean; +} + +export type MenuItemNode = { + label: string; + key: string; + hidden?: boolean; + icon?: any; + action?: UrlActionType, + children?: MenuItemNode[]; +} + +function checkDataNodes(value: any, key?: string): MenuItemNode[] | undefined { + return check(value, ["array", "undefined"], key, (node, k) => { + check(node, ["object"], k); + check(node["label"], ["string"], "label"); + check(node["hidden"], ["boolean", "undefined"], "hidden"); + check(node["icon"], ["string", "undefined"], "icon"); + check(node["action"], ["object", "undefined"], "action"); + checkDataNodes(node["children"], "children"); + return node; + }); +} + +function convertTreeData(data: any) { + return data === "" ? [] : checkDataNodes(data) ?? []; +} + let NavTmpLayout = (function () { const childrenMap = { + dataOptionType: dropdownControl(DataOptionType, DataOption.Manual), items: withDefault(LayoutMenuItemListComp, [ { label: trans("menuItem") + " 1", + itemKey: genRandomKey(), }, ]), + jsonItems: jsonControl(convertTreeData, jsonMenuItems), + width: withDefault(StringControl, DEFAULT_WIDTH), + backgroundImage: withDefault(StringControl, ""), + mode: dropdownControl(ModeOptions, "inline"), + navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle), + navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle), + navItemHoverStyle: withDefault(styleControl(NavLayoutItemHoverStyle), {}), + navItemActiveStyle: withDefault(styleControl(NavLayoutItemActiveStyle), {}), }; return new MultiCompBuilder(childrenMap, (props) => { return null; }) .setPropertyViewFn((children) => { + const [styleSegment, setStyleSegment] = useState('normal') + return ( - <> - <Section name={trans("menu")}>{menuPropertyView(children.items)}</Section> - </> + <div style={{overflowY: 'auto'}}> + <Section name={trans("menu")}> + {children.dataOptionType.propertyView({ + radioButton: true, + type: "oneline", + })} + { + children.dataOptionType.getView() === DataOption.Manual + ? menuPropertyView(children.items) + : children.jsonItems.propertyView({ + label: "Json Data", + }) + } + </Section> + <Section name={sectionNames.layout}> + { children.width.propertyView({ + label: trans("navLayout.width"), + tooltip: trans("navLayout.widthTooltip"), + placeholder: DEFAULT_WIDTH + "", + })} + { children.mode.propertyView({ + label: trans("labelProp.position"), + radioButton: true + })} + {children.backgroundImage.propertyView({ + label: `Background Image`, + placeholder: 'https://temp.im/350x400', + })} + </Section> + <Section name={trans("navLayout.navStyle")}> + { children.navStyle.getPropertyView() } + </Section> + <Section name={trans("navLayout.navItemStyle")}> + {controlItem({}, ( + <Segmented + block + options={menuItemStyleOptions} + value={styleSegment} + onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)} + /> + ))} + {styleSegment === 'normal' && ( + children.navItemStyle.getPropertyView() + )} + {styleSegment === 'hover' && ( + children.navItemHoverStyle.getPropertyView() + )} + {styleSegment === 'active' && ( + children.navItemActiveStyle.getPropertyView() + )} + </Section> + </div> ); }) .build(); @@ -64,13 +260,98 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const pathParam = useAppPathParam(); const isViewMode = isUserViewMode(pathParam); const [selectedKey, setSelectedKey] = useState(""); - const items = useMemo(() => comp.children.items.getView(), [comp.children.items]); - + const items = comp.children.items.getView(); + const navWidth = comp.children.width.getView(); + const navMode = comp.children.mode.getView(); + const navStyle = comp.children.navStyle.getView(); + const navItemStyle = comp.children.navItemStyle.getView(); + const navItemHoverStyle = comp.children.navItemHoverStyle.getView(); + const navItemActiveStyle = comp.children.navItemActiveStyle.getView(); + const backgroundImage = comp.children.backgroundImage.getView(); + const jsonItems = comp.children.jsonItems.getView(); + const dataOptionType = comp.children.dataOptionType.getView(); + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); }, []); + const generateItemKeyRecord = useCallback( + (items: LayoutMenuItemComp[] | MenuItemNode[]) => { + const result: Record<string, LayoutMenuItemComp | MenuItemNode> = {}; + if(dataOptionType === DataOption.Manual) { + (items as LayoutMenuItemComp[])?.forEach((item) => { + const subItems = item.children.items.getView(); + if (subItems.length > 0) { + Object.assign(result, generateItemKeyRecord(subItems)) + } + result[item.getItemKey()] = item; + }); + } + if(dataOptionType === DataOption.Json) { + (items as MenuItemNode[])?.forEach((item) => { + if (item.children?.length) { + Object.assign(result, generateItemKeyRecord(item.children)) + } + result[item.key] = item; + }) + } + return result; + }, [dataOptionType] + ) + + const itemKeyRecord = useMemo(() => { + if(dataOptionType === DataOption.Json) { + return generateItemKeyRecord(jsonItems) + } + return generateItemKeyRecord(items) + }, [dataOptionType, jsonItems, items, generateItemKeyRecord]); + + const onMenuItemClick = useCallback(({key}: {key: string}) => { + const itemComp = itemKeyRecord[key] + + const url = [ + ALL_APPLICATIONS_URL, + pathParam.applicationId, + pathParam.viewMode, + key, + ].join("/"); + + // handle manual menu item action + if(dataOptionType === DataOption.Manual) { + (itemComp as LayoutMenuItemComp).children.action.act(url); + return; + } + // handle json menu item action + if((itemComp as MenuItemNode).action?.newTab) { + return window.open((itemComp as MenuItemNode).action?.url, '_blank') + } + history.push(url); + }, [pathParam.applicationId, pathParam.viewMode, dataOptionType, itemKeyRecord]) + + const getJsonMenuItem = useCallback( + (items: MenuItemNode[]): MenuProps["items"] => { + return items?.map((item: MenuItemNode) => { + const { + label, + key, + hidden, + icon, + children, + } = item; + return { + label, + key, + hidden, + icon: <StyledImage src={icon} />, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, + ...(children?.length && { children: getJsonMenuItem(children) }), + } + }) + }, [onMenuItemClick] + ) + const getMenuItem = useCallback( (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => { return itemComps.filter(filterItem).map((item) => { @@ -81,14 +362,20 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { title: label, key: item.getItemKey(), icon: <span>{item.children.icon.getView()}</span>, + onTitleClick: onMenuItemClick, + onClick: onMenuItemClick, ...(subItems.length > 0 && { children: getMenuItem(subItems) }), }; }); }, - [filterItem] + [onMenuItemClick, filterItem] ); - const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]); + const menuItems = useMemo(() => { + if(dataOptionType === DataOption.Json) return getJsonMenuItem(jsonItems) + + return getMenuItem(items) + }, [dataOptionType, jsonItems, getJsonMenuItem, items, getMenuItem]); // Find by path itemKey const findItemPathByKey = useCallback( @@ -134,22 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { [filterItem] ); - const itemKeyRecord = useMemo(() => { - const result: Record<string, LayoutMenuItemComp> = {}; - items.forEach((item) => { - const subItems = item.children.items.getView(); - if (subItems.length > 0) { - item.children.items - .getView() - .forEach((subItem) => (result[subItem.getItemKey()] = subItem)); - } else { - result[item.getItemKey()] = item; + // Find by path itemKey + const findItemPathByKeyJson = useCallback( + (itemComps: MenuItemNode[], itemKey: string): string[] => { + for (let item of itemComps) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findItemPathByKeyJson(subItems, itemKey); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + if (item.key === itemKey) { + return [item.key]; + } + } + } + return []; + }, + [] + ); + + // Get the first visible menu + const findFirstItemPathJson = useCallback( + (itemComps: MenuItemNode[]): string[] => { + for (let item of itemComps) { + if (!item.hidden) { + const subItems = item.children; + if (subItems?.length) { + // have subMenus + const childPath = findFirstItemPathJson(subItems); + if (childPath.length > 0) { + return [item.key, ...childPath]; + } + } else { + return [item.key]; + } + } } - }); - return result; - }, [items]); + return []; + }, [] + ); const defaultOpenKeys = useMemo(() => { + if(dataOptionType === DataOption.Json) { + let itemPath: string[]; + if (pathParam.appPageId) { + itemPath = findItemPathByKeyJson(jsonItems, pathParam.appPageId); + } else { + itemPath = findFirstItemPathJson(jsonItems); + } + return itemPath.slice(0, itemPath.length - 1); + } + let itemPath: string[]; if (pathParam.appPageId) { itemPath = findItemPathByKey(items, pathParam.appPageId); @@ -170,34 +495,79 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { setSelectedKey(selectedKey); }, [pathParam.appPageId]); - let pageView = <EmptyContent text="" style={{ height: "100%" }} />; - const selectedItem = itemKeyRecord[selectedKey]; - if (selectedItem && !selectedItem.children.hidden.getView()) { - const compView = selectedItem.children.action.getView(); - if (compView) { - pageView = compView; + const pageView = useMemo(() => { + let pageView = <EmptyContent text="" style={{ height: "100%" }} />; + + if(dataOptionType === DataOption.Manual) { + const selectedItem = (itemKeyRecord[selectedKey] as LayoutMenuItemComp); + if (selectedItem && !selectedItem.children.hidden.getView()) { + const compView = selectedItem.children.action.getView(); + if (compView) { + pageView = compView; + } + } + } + if(dataOptionType === DataOption.Json) { + const item = (itemKeyRecord[selectedKey] as MenuItemNode) + if(item?.action?.url) { + pageView = <iframe + title={item?.action?.url} + src={item?.action?.url} + width="100%" + height="100%" + style={{ border: "none", marginBottom: "-6px" }} + /> + } } + return pageView; + }, [dataOptionType, itemKeyRecord, selectedKey]) + + const getVerticalMargin = (margin: string[]) => { + if(margin.length === 1) return `${margin[0]}`; + if(margin.length === 2) return `(${margin[0]} + ${margin[0]})`; + if(margin.length === 3 || margin.length === 4) + return `(${margin[0]} + ${margin[2]})`; + + return '0px'; + } + const getHorizontalMargin = (margin: string[]) => { + if(margin.length === 1) return `(${margin[0]} + ${margin[0]})`; + if(margin.length === 2) return `(${margin[1]} + ${margin[1]})`; + if(margin.length === 3 || margin.length === 4) + return `(${margin[1]} + ${margin[3]})`; + + return '0px'; + } + + let backgroundStyle = navStyle.background; + if(!_.isEmpty(backgroundImage)) { + backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`; } let content = ( <Layout> - <StyledSide theme="light" width={240}> - <AntdMenu + <StyledSide theme="light" width={navWidth}> + <StyledMenu items={menuItems} - mode="inline" - style={{ height: "100%" }} + mode={navMode} + style={{ + height: `calc(100% - ${getVerticalMargin(navStyle.margin.split(' '))})`, + width: `calc(100% - ${getHorizontalMargin(navStyle.margin.split(' '))})`, + borderRight: `1px solid ${navStyle.border}`, + borderRadius: navStyle.radius, + color: navStyle.text, + margin: navStyle.margin, + padding: navStyle.padding, + background: backgroundStyle, + }} defaultOpenKeys={defaultOpenKeys} selectedKeys={[selectedKey]} - onClick={(e) => { - const itemComp = itemKeyRecord[e.key]; - const url = [ - ALL_APPLICATIONS_URL, - pathParam.applicationId, - pathParam.viewMode, - itemComp.getItemKey(), - ].join("/"); - itemComp.children.action.act(url); + $navItemStyle={{ + width: `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`, + ...navItemStyle, }} + $navItemHoverStyle={navItemHoverStyle} + $navItemActiveStyle={navItemActiveStyle} /> </StyledSide> <MainContent>{pageView}</MainContent> diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts new file mode 100644 index 000000000..e8fc23c0b --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts @@ -0,0 +1,77 @@ +import { trans } from "i18n"; + +export const ModeOptions = [ + { label: trans("navLayout.modeInline"), value: "inline" }, + { label: trans("navLayout.modeVertical"), value: "vertical" }, +] as const; + +export const DataOption = { + Manual: 'manual', + Json: 'json', +} +export const DataOptionType = [ + { + label: trans("prop.manual"), + value: DataOption.Manual, + }, + { + label: trans("prop.json"), + value: DataOption.Json, + }, +]; + +export const menuItemStyleOptions = [ + { + value: "normal", + label: "Normal", + }, + { + value: "hover", + label: "Hover", + }, + { + value: "active", + label: "Active", + } +]; + +export const jsonMenuItems = [ + { + label: "Menu Item 1", + key: 'menu-item-1', + icon: "https://cdn-icons-png.flaticon.com/128/149/149338.png", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + children: [ + { + label: "Submenu Item 1", + key: 'submenu-item-11', + icon: "", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + }, + { + label: "Submenu Item 2", + key: 'submenu-item-12', + icon: "", + action: { + url: "https://www.lowcoder.cloud", + newTab: false, + }, + }, + ] + }, + { + label: "Menu Item 2", + key: 'menu-item-2', + icon: "https://cdn-icons-png.flaticon.com/128/149/149206.png", + action: { + url: "https://www.lowcoder.cloud", + newTab: true, + }, + } +] \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx index 44c342101..c4f22191a 100644 --- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx @@ -1,10 +1,12 @@ import { useDraggable, useDroppable } from "@dnd-kit/core"; import { trans } from "i18n"; -import { Fragment } from "react"; +import { Fragment, useEffect } from "react"; import styled from "styled-components"; import DroppablePlaceholder from "./DroppablePlaceHolder"; import MenuItem, { ICommonItemProps } from "./MenuItem"; import { IDragData, IDropData } from "./types"; +import { LayoutMenuItemComp } from "comps/comps/layout/layoutMenuItemComp"; +import { genRandomKey } from "comps/utils/idGenerator"; const DraggableMenuItemWrapper = styled.div` position: relative; @@ -63,6 +65,22 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { disabled: isDragging || disabled || disableDropIn, data: dropData, }); + + // TODO: Remove this later. + // Set ItemKey for previously added sub-menus + useEffect(() => { + if(!items.length) return; + if(!(items[0] instanceof LayoutMenuItemComp)) return; + + return items.forEach(item => { + const subItem = item as LayoutMenuItemComp; + const itemKey = subItem.children.itemKey.getView(); + if(itemKey === '') { + subItem.children.itemKey.dispatchChangeValueAction(genRandomKey()) + } + }) + }, [items]) + return ( <> <DraggableMenuItemWrapper> @@ -99,7 +117,7 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { item={subItem} level={0} disabled={disabled || isDragging || disableDropIn} - // onAddSubMenu={onAddSubMenu} + onAddSubMenu={onAddSubMenu} onDelete={onDelete} parentDragging={isDragging} /> diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 0344f21c9..58b706771 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -821,7 +821,7 @@ export const TreeStyle = [ export const TreeSelectStyle = [...multiSelectCommon, ...ACCENT_VALIDATE] as const; -export const DrawerStyle = [getBackground()] as const; +export const DrawerStyle = [getBackground()] as const export const JsonEditorStyle = [LABEL] as const; @@ -928,6 +928,59 @@ export const ResponsiveLayoutColStyle = [ PADDING, ] as const; +export const NavLayoutStyle = [ + ...getBgBorderRadiusByBg(), + { + name: "text", + label: trans("text"), + depName: "background", + // depTheme: "primary", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + MARGIN, + PADDING, +] as const; + +export const NavLayoutItemStyle = [ + getBackground("primarySurface"), + getStaticBorder('transparent'), + RADIUS, + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, + MARGIN, + PADDING, +] as const; + +export const NavLayoutItemHoverStyle = [ + getBackground("canvas"), + getStaticBorder('transparent'), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + +export const NavLayoutItemActiveStyle = [ + getBackground("primary"), + getStaticBorder('transparent'), + { + name: "text", + label: trans("text"), + depName: "background", + depType: DEP_TYPE.CONTRAST_TEXT, + transformer: contrastText, + }, +] as const; + export const CarouselStyle = [getBackground("canvas")] as const; export const RichTextEditorStyle = [getStaticBorder(), RADIUS] as const; @@ -968,6 +1021,10 @@ export type CarouselStyleType = StyleConfigType<typeof CarouselStyle>; export type RichTextEditorStyleType = StyleConfigType<typeof RichTextEditorStyle>; export type ResponsiveLayoutRowStyleType = StyleConfigType<typeof ResponsiveLayoutRowStyle>; export type ResponsiveLayoutColStyleType = StyleConfigType<typeof ResponsiveLayoutColStyle>; +export type NavLayoutStyleType = StyleConfigType<typeof NavLayoutStyle>; +export type NavLayoutItemStyleType = StyleConfigType<typeof NavLayoutItemStyle>; +export type NavLayoutItemHoverStyleType = StyleConfigType<typeof NavLayoutItemHoverStyle>; +export type NavLayoutItemActiveStyleType = StyleConfigType<typeof NavLayoutItemActiveStyle>; export function widthCalculator(margin: string) { const marginArr = margin?.trim().replace(/\s+/g,' ').split(" ") || ""; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 4207f3ac6..ade1d8355 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -177,6 +177,7 @@ export const en = { "If the result is non-empty string, it is an error message. If empty or null, the validation passes. Example: ", manual: "Manual", map: "Mapped", + json: "JSON", use12Hours: "Use 12-hours", hourStep: "Hour step", minuteStep: "Minute step", @@ -2713,4 +2714,13 @@ export const en = { rowLayout: "Row Layout", columnsLayout: "Columns Layout", }, + navLayout: { + mode: "Mode", + modeInline: "Inline", + modeVertical: "Vertical", + width: "Width", + widthTooltip: "Number or percentage, e.g. 520, 60%", + navStyle: "Menu Style", + navItemStyle: "Menu Item Style", + } }; diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 5d10e78f1..6b97f4805 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -167,6 +167,7 @@ prop: { customRuleTooltip: "如果结果是非空字符串,则为错误消息.如果为空或null,则验证通过.\n示例:", manual: "手动", map: "映射", + json: "JSON", use12Hours: "使用12小时制", hourStep: "小时步长", minuteStep: "分钟步长", @@ -2559,6 +2560,15 @@ timeLine: { matchColumnsHeight: "匹配列高度", rowLayout: "行布局", columnsLayout: "栏目布局", + }, + navLayout: { + mode: "模式", + modeInline: "排队", + modeVertical: "垂直的", + width: "宽度", + widthTooltip: "数字或百分比,例如 520,60%", + navStyle: "菜单风格", + navItemStyle: "菜单项样式", } };