From 616d61e3317f43f78cd026dc1720fb6a63f58f5b Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 20 Apr 2026 22:02:26 +0500 Subject: [PATCH 01/14] [Feat]: #2124 add customization for the progress circle --- .../src/comps/comps/progressCircleComp.tsx | 123 +++++++++++++++--- .../comps/comps/progressCircleConstants.ts | 22 ++++ .../packages/lowcoder/src/i18n/locales/en.ts | 33 +++++ 3 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/progressCircleConstants.ts diff --git a/client/packages/lowcoder/src/comps/comps/progressCircleComp.tsx b/client/packages/lowcoder/src/comps/comps/progressCircleComp.tsx index b2070c7c2..e4e70a632 100644 --- a/client/packages/lowcoder/src/comps/comps/progressCircleComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/progressCircleComp.tsx @@ -3,16 +3,21 @@ import { styleControl } from "comps/controls/styleControl"; import { AnimationStyle, AnimationStyleType, CircleProgressStyle, CircleProgressType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import styled, { css } from "styled-components"; import { Section, sectionNames } from "lowcoder-design"; -import { numberExposingStateControl } from "../controls/codeStateControl"; +import { numberExposingStateControl, stringExposingStateControl } from "../controls/codeStateControl"; import { UICompBuilder } from "../generators"; import { NameConfig, NameConfigHidden, withExposingConfigs } from "../generators/withExposing"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; - +import { BoolControl } from "../controls/boolControl"; +import { dropdownControl } from "../controls/dropdownControl"; +import { NumberControl } from "../controls/codeControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; - -// TODO: after Update of ANTd, introduce Size attribute to ProgressCircle +import { + ProgressTypeOptions, + StrokeLinecapOptions, + GapPositionOptions +} from "./progressCircleConstants"; const getStyle = (style: CircleProgressType) => { return css` @@ -20,13 +25,13 @@ const getStyle = (style: CircleProgressType) => { height: ${heightCalculator(style.margin)}; margin: ${style.margin}; padding: ${style.padding}; - border-radius:${style.radius}; + border-radius: ${style.radius}; .ant-progress-text { color: ${style.text} !important; - font-family:${style.fontFamily}; - font-style:${style.fontStyle}; - font-size:${style.textSize} !important; - font-weight:${style.textWeight}; + font-family: ${style.fontFamily}; + font-style: ${style.fontStyle}; + font-size: ${style.textSize} !important; + font-weight: ${style.textWeight}; } .ant-progress-circle-trail { stroke: ${style.track}; @@ -68,21 +73,54 @@ export const StyledProgressCircle = styled(Progress)<{ let ProgressCircleTmpComp = (function () { const childrenMap = { value: numberExposingStateControl("value", 60), - // borderRadius property hidden as it's not valid for progress circle + progressType: dropdownControl(ProgressTypeOptions, "circle"), + showInfo: BoolControl.DEFAULT_TRUE, + strokeWidth: NumberControl, + strokeLinecap: dropdownControl(StrokeLinecapOptions, "round"), + gapDegree: NumberControl, + gapPosition: dropdownControl(GapPositionOptions, "bottom"), + customFormat: stringExposingStateControl("customFormat", ""), + // Steps configuration for segmented progress + stepsEnabled: BoolControl, + stepsCount: NumberControl, + stepsGap: NumberControl, + // Style controls style: styleControl(CircleProgressStyle, 'style'), animationStyle: styleControl(AnimationStyle, 'animationStyle'), }; + return new UICompBuilder(childrenMap, (props) => { + const percent = Math.round(props.value.value); + const customFormatValue = props.customFormat.value?.trim(); + + // Simple format function - just returns the custom text if provided + const formatFunction = customFormatValue ? () => customFormatValue : undefined; + + // Build steps configuration if enabled + const stepsConfig = props.stepsEnabled && props.stepsCount > 0 + ? { count: props.stepsCount, gap: props.stepsGap || 2 } + : undefined; + return ( ); }) .setPropertyViewFn((children) => { + const progressType = children.progressType.getView(); + const stepsEnabled = children.stepsEnabled.getView(); + return ( <>
@@ -90,8 +128,62 @@ let ProgressCircleTmpComp = (function () { label: trans("progress.value"), tooltip: trans("progress.valueTooltip"), })} + {children.progressType.propertyView({ + label: trans("progressCircle.progressType"), + tooltip: trans("progressCircle.progressTypeTooltip"), + })} +
+ +
+ {children.showInfo.propertyView({ + label: trans("progress.showInfo"), + })} + {children.customFormat.propertyView({ + label: trans("progressCircle.customFormat"), + tooltip: trans("progressCircle.customFormatTooltip"), + })} + {children.strokeWidth.propertyView({ + label: trans("progressCircle.strokeWidth"), + tooltip: trans("progressCircle.strokeWidthTooltip"), + placeholder: "6", + })} + {children.strokeLinecap.propertyView({ + label: trans("progressCircle.strokeLinecap"), + tooltip: trans("progressCircle.strokeLinecapTooltip"), + })} +
+ +
+ {children.stepsEnabled.propertyView({ + label: trans("progressCircle.stepsEnabled"), + tooltip: trans("progressCircle.stepsEnabledTooltip"), + })} + {stepsEnabled && children.stepsCount.propertyView({ + label: trans("progressCircle.stepsCount"), + tooltip: trans("progressCircle.stepsCountTooltip"), + placeholder: "5", + })} + {stepsEnabled && children.stepsGap.propertyView({ + label: trans("progressCircle.stepsGap"), + tooltip: trans("progressCircle.stepsGapTooltip"), + placeholder: "2", + })}
+ {progressType === "dashboard" && ( +
+ {children.gapDegree.propertyView({ + label: trans("progressCircle.gapDegree"), + tooltip: trans("progressCircle.gapDegreeTooltip"), + placeholder: "75", + })} + {children.gapPosition.propertyView({ + label: trans("progressCircle.gapPosition"), + tooltip: trans("progressCircle.gapPositionTooltip"), + })} +
+ )} + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && (
{hiddenPropertyView(children)} @@ -101,12 +193,12 @@ let ProgressCircleTmpComp = (function () { {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <>
- {children.style.getPropertyView()} + {children.style.getPropertyView()}
- {children.animationStyle.getPropertyView()} + {children.animationStyle.getPropertyView()}
- + )} ); @@ -122,5 +214,6 @@ ProgressCircleTmpComp = class extends ProgressCircleTmpComp { export const ProgressCircleComp = withExposingConfigs(ProgressCircleTmpComp, [ new NameConfig("value", trans("progress.valueDesc")), + new NameConfig("customFormat", trans("progressCircle.customFormatDesc")), NameConfigHidden, ]); diff --git a/client/packages/lowcoder/src/comps/comps/progressCircleConstants.ts b/client/packages/lowcoder/src/comps/comps/progressCircleConstants.ts new file mode 100644 index 000000000..31a5bb112 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/progressCircleConstants.ts @@ -0,0 +1,22 @@ +import { trans } from "i18n"; + +// Progress type options (circle or dashboard) +export const ProgressTypeOptions = [ + { label: trans("progressCircle.circle"), value: "circle" }, + { label: trans("progressCircle.dashboard"), value: "dashboard" }, +] as const; + +// Stroke linecap options (line ending style) +export const StrokeLinecapOptions = [ + { label: trans("progressCircle.round"), value: "round" }, + { label: trans("progressCircle.butt"), value: "butt" }, + { label: trans("progressCircle.square"), value: "square" }, +] as const; + +// Gap position options (for dashboard type) +export const GapPositionOptions = [ + { label: trans("progressCircle.top"), value: "top" }, + { label: trans("progressCircle.bottom"), value: "bottom" }, + { label: trans("progressCircle.left"), value: "left" }, + { label: trans("progressCircle.right"), value: "right" }, +] as const; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index e9dcf1268..95488cc8c 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2257,6 +2257,39 @@ export const en = { "valueDesc": "Current Progress Value, Ranging from 0 to 100", "showInfoDesc": "Whether to Display the Current Progress Value" }, + "progressCircle": { + "circle": "Circle", + "dashboard": "Dashboard", + "progressType": "Type", + "progressTypeTooltip": "Choose between a full circle or dashboard (semi-circle) style", + "appearance": "Appearance", + "customFormat": "Custom Text", + "customFormatTooltip": "Custom text to display instead of the percentage", + "customFormatDesc": "Custom text displayed in the progress circle", + "strokeWidth": "Stroke Width", + "strokeWidthTooltip": "The width of the progress stroke line (default: 6)", + "strokeLinecap": "Line Cap Style", + "strokeLinecapTooltip": "The shape of the progress line endings", + "round": "Round", + "butt": "Flat", + "square": "Square", + "dashboardSettings": "Dashboard Settings", + "gapDegree": "Gap Degree", + "gapDegreeTooltip": "The gap degree of the dashboard, 0-295 (default: 75)", + "gapPosition": "Gap Position", + "gapPositionTooltip": "The position of the gap in the dashboard", + "top": "Top", + "bottom": "Bottom", + "left": "Left", + "right": "Right", + "segments": "Segments", + "stepsEnabled": "Enable Steps", + "stepsEnabledTooltip": "Display progress as segmented steps instead of continuous", + "stepsCount": "Step Count", + "stepsCountTooltip": "The total number of steps/segments to display", + "stepsGap": "Step Gap", + "stepsGapTooltip": "The gap between each step in pixels (default: 2)" + }, "fileViewer": { "invalidURL": "Please Enter a Valid URL or Base64 String", "src": "File URI", From 4164f33607dc63efcfcf2dc4ea06a63d6d0e1902 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 21 Apr 2026 18:21:25 +0500 Subject: [PATCH 02/14] [Feat]: #2145 add theme settings for the Navigation App --- .../src/comps/comps/appSettingsComp.tsx | 18 +++++-- .../comps/comps/layout/mobileTabLayout.tsx | 45 ++++++++++++++++-- .../src/comps/comps/layout/navLayout.tsx | 47 +++++++++++++++++-- .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../lowcoder/src/pages/editor/editorView.tsx | 17 +++---- 5 files changed, 108 insertions(+), 20 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx index 64122daba..0ee806c18 100644 --- a/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/appSettingsComp.tsx @@ -342,6 +342,14 @@ function AppGeneralSettingsModal(props: ChildrenInstance) { function AppCanvasSettingsModal(props: ChildrenInstance) { const isPublicApp = useSelector(isPublicApplication); + const application = useSelector(currentApplication); + // Aggregation apps (PC Navigation / Mobile Navigation) do not use the + // grid CanvasView, so grid columns / row height / row count / canvas max + // width have no visual effect for them. Only the theme + background + + // padding fields apply. + const isAggregation = !!application && isAggregationApp( + AppUILayoutType[application.applicationType] + ); const { themeList, defaultTheme, @@ -397,7 +405,7 @@ function AppCanvasSettingsModal(props: ChildrenInstance) { return ( <> - {maxWidth.propertyView({ + {!isAggregation && maxWidth.propertyView({ dropdownLabel: trans("appSetting.canvasMaxWidth"), inputLabel: trans("appSetting.userDefinedMaxWidth"), inputPlaceholder: trans("appSetting.inputUserDefinedPxValue"), @@ -462,15 +470,15 @@ function AppCanvasSettingsModal(props: ChildrenInstance) { min: 350, lastNode: {trans("appSetting.maxWidthTip")}, })} - {gridColumns.propertyView({ + {!isAggregation && gridColumns.propertyView({ label: trans("appSetting.gridColumns"), placeholder: '24', })} - {gridRowHeight.propertyView({ + {!isAggregation && gridRowHeight.propertyView({ label: trans("appSetting.gridRowHeight"), placeholder: '8', })} - {gridRowCount.propertyView({ + {!isAggregation && gridRowCount.propertyView({ label: trans("appSetting.gridRowCount"), placeholder: 'Infinity', })} diff --git a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx index 8ae653ffa..0b8b129d3 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/mobileTabLayout.tsx @@ -16,6 +16,7 @@ import { PreviewContainerID } from "constants/domLocators"; import { EditorContainer, EmptyContent } from "pages/common/styledComponent"; import { Layers } from "constants/Layers"; import { ExternalEditorContext } from "util/context/ExternalEditorContext"; +import { EditorContext } from "comps/editorState"; import { default as Skeleton } from "antd/es/skeleton"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl"; @@ -664,6 +665,34 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { const bgColor = (useContext(ThemeContext)?.theme || defaultTheme).canvas; const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). Mobile nav already + // owns its own maxWidth + grid behaviour, so we only consume the + // background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + + const canvasBackgroundStyle: React.CSSProperties = {}; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + const canvasContentPadding = `${canvasPaddingY}px ${canvasPaddingX}px`; + const getContainer = useCallback(() => document.querySelector(`#${PreviewContainerID}`) || document.querySelector(`#${CanvasContainerID}`) || @@ -870,8 +899,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { if (readOnly) { return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} @@ -885,8 +918,12 @@ MobileTabLayoutTmp = withViewFn(MobileTabLayoutTmp, (comp) => { } return ( - - {appView} + + {appView} {menuMode === MobileMode.Hamburger ? ( <> {hamburgerButton} diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx index 4a7e2b355..66f23635c 100644 --- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx @@ -6,6 +6,7 @@ import MainContent from "components/layout/MainContent"; import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp"; import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList"; import { registerLayoutMap } from "comps/comps/uiComp"; +import { EditorContext } from "comps/editorState"; import { MultiCompBuilder, withDefault, withViewFn } from "comps/generators"; import { withDispatchHook } from "comps/generators/withDispatchHook"; import { NameAndExposingInfo } from "comps/utils/exposingTypes"; @@ -14,7 +15,7 @@ import { TopHeaderHeight } from "constants/style"; 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 { useCallback, useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { isUserViewMode, useAppPathParam } from "util/hooks"; import { StringControl, jsonControl } from "comps/controls/codeControl"; @@ -381,6 +382,21 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { const dataOptionType = comp.children.dataOptionType.getView(); const onEvent = comp.children.onEvent.getView(); + // Pull app-level Theme / Canvas Settings (managed via the left-sidebar + // "Canvas" pane and shared with normal apps + modules). For aggregation + // apps the grid sizing fields are intentionally hidden in the settings UI; + // we only consume the background + padding subset here. + const editorState = useContext(EditorContext); + const appSettings = editorState?.getAppSettings(); + const canvasBg = appSettings?.gridBg; + const canvasBgImage = appSettings?.gridBgImage; + const canvasBgImageRepeat = appSettings?.gridBgImageRepeat || "no-repeat"; + const canvasBgImageSize = appSettings?.gridBgImageSize || "cover"; + const canvasBgImagePosition = appSettings?.gridBgImagePosition || "center"; + const canvasBgImageOrigin = appSettings?.gridBgImageOrigin || "padding-box"; + const canvasPaddingX = appSettings?.gridPaddingX ?? 0; + const canvasPaddingY = appSettings?.gridPaddingY ?? 0; + // filter out hidden. unauthorised items filtered by server const filterItem = useCallback((item: LayoutMenuItemComp): boolean => { return !item.children.hidden.getView(); @@ -685,8 +701,25 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { /> ); + // Build canvas background style (color + optional image), driven by the + // shared app-level Canvas Settings. + const canvasBackgroundStyle: React.CSSProperties = {}; + if (canvasBg) { + canvasBackgroundStyle.background = canvasBg; + } + if (canvasBgImage) { + canvasBackgroundStyle.backgroundImage = `url('${canvasBgImage}')`; + canvasBackgroundStyle.backgroundRepeat = canvasBgImageRepeat; + canvasBackgroundStyle.backgroundSize = canvasBgImageSize; + canvasBackgroundStyle.backgroundPosition = canvasBgImagePosition; + canvasBackgroundStyle.backgroundOrigin = canvasBgImageOrigin; + } + let content = ( - + {(navPosition === 'top') && (
{ navMenu } @@ -697,7 +730,15 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => { {navMenu} )} - {pageView} + + {pageView} + {(navPosition === 'bottom') && (