diff --git a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx index 7b9c80a24..d5b4f6f52 100644 --- a/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx +++ b/client/packages/lowcoder/src/comps/comps/columnLayout/columnLayout.tsx @@ -43,20 +43,34 @@ import { DisabledContext } from "comps/generators/uiCompBuilder"; import { SliderControl } from "@lowcoder-ee/comps/controls/sliderControl"; import { getBackgroundStyle } from "@lowcoder-ee/util/styleUtils"; +// Extended ContainerStyleType for our specific needs +interface ExtendedContainerStyleType extends ContainerStyleType { + display?: string; + gridTemplateColumns?: string; + gridTemplateRows?: string; + columnGap?: string; + rowGap?: string; + [key: string]: string | undefined; +} + const ContainWrapper = styled.div<{ - $style: ContainerStyleType & { - display: string, - gridTemplateColumns: string, - columnGap: string, - gridTemplateRows: string, - rowGap: string, - } | undefined; + $style: ExtendedContainerStyleType | undefined; + $useFlexLayout: boolean; }>` - display: ${(props) => props.$style?.display}; - grid-template-columns: ${(props) => props.$style?.gridTemplateColumns}; - grid-template-rows: ${(props) => props.$style?.gridTemplateRows}; - column-gap: ${(props) => props.$style?.columnGap}; - row-gap: ${(props) => props.$style?.rowGap}; + display: ${(props) => props.$useFlexLayout ? 'flex' : props.$style?.display}; + flex-wrap: ${(props) => props.$useFlexLayout ? 'wrap' : 'nowrap'}; + + ${(props) => !props.$useFlexLayout && ` + grid-template-columns: ${props.$style?.gridTemplateColumns}; + grid-template-rows: ${props.$style?.gridTemplateRows}; + column-gap: ${props.$style?.columnGap}; + row-gap: ${props.$style?.rowGap}; + `} + + ${(props) => props.$useFlexLayout && ` + column-gap: ${props.$style?.columnGap || '0'}; + row-gap: ${props.$style?.rowGap || '0'}; + `} border-radius: ${(props) => props.$style?.radius}; border-width: ${(props) => props.$style?.borderWidth}; @@ -67,11 +81,29 @@ const ContainWrapper = styled.div<{ ${props => props.$style && getBackgroundStyle(props.$style)} `; -const ColWrapper = styled(Col)<{ +const getColumnWidth = (column: any): string => { + // Use explicit width if available + if (column.width) { + return column.width; + } + + // No explicit width - return auto to let flex handle it + return 'auto'; +}; + +const ColWrapper = styled.div<{ $style: ResponsiveLayoutColStyleType | undefined, - $minWidth?: string, + $width: string, $matchColumnsHeight: boolean, + $useFlexLayout: boolean, + $hasExplicitWidth: boolean, }>` + ${props => props.$useFlexLayout ? ` + ${props.$hasExplicitWidth + ? `flex: 0 0 ${props.$width}; max-width: ${props.$width};` + : 'flex: 1 1 0%; min-width: 0;'} + ` : ''} + > div { height: ${(props) => props.$matchColumnsHeight ? `calc(100% - ${props.$style?.padding || 0} - ${props.$style?.padding || 0})` : 'auto'}; border-radius: ${(props) => props.$style?.radius}; @@ -95,12 +127,13 @@ const childrenMap = { autoHeight: AutoHeightControl, matchColumnsHeight: withDefault(BoolControl, true), templateRows: withDefault(StringControl, "1fr"), - rowGap: withDefault(StringControl, "20px"), + rowGap: withDefault(StringControl, "0"), templateColumns: withDefault(StringControl, "1fr 1fr"), mainScrollbar: withDefault(BoolControl, false), - columnGap: withDefault(StringControl, "20px"), + columnGap: withDefault(StringControl, "0"), style: styleControl(ContainerStyle, 'style'), - columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle') + columnStyle: styleControl(ResponsiveLayoutColStyle , 'columnStyle'), + useFlexLayout: withDefault(BoolControl, false), }; type ViewProps = RecordConstructorToView; @@ -122,7 +155,6 @@ const ColumnContainer = (props: ColumnContainerProps) => { ); }; - const ColumnLayout = (props: ColumnLayoutProps) => { let { columns, @@ -135,7 +167,8 @@ const ColumnLayout = (props: ColumnLayoutProps) => { columnGap, columnStyle, horizontalGridCells, - mainScrollbar + mainScrollbar, + useFlexLayout, } = props; return ( @@ -143,27 +176,33 @@ const ColumnLayout = (props: ColumnLayoutProps) => {
- + {columns.map(column => { const id = String(column.id); const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id); if(!containers[id]) return null const containerProps = containers[id].children; - const noOfColumns = columns.length; + const columnWidth = getColumnWidth(column); + const hasExplicitWidth = !!column.width; + return ( - + @@ -220,6 +260,10 @@ export const ResponsiveLayoutBaseComp = (function () { {children.horizontalGridCells.propertyView({ label: trans('prop.horizontalGridCells'), })} + {children.useFlexLayout.propertyView({ + label: trans("responsiveLayout.useFlexLayout"), + tooltip: trans("responsiveLayout.useFlexLayoutTooltip") + })}
{children.matchColumnsHeight.propertyView({ label: trans("responsiveLayout.matchColumnsHeight") @@ -227,8 +271,14 @@ export const ResponsiveLayoutBaseComp = (function () { {controlItem({}, (
{trans("responsiveLayout.columnsSpacing")}
))} - {children.templateColumns.propertyView({label: trans("responsiveLayout.columnDefinition"), tooltip: trans("responsiveLayout.columnsDefinitionTooltip")})} - {children.templateRows.propertyView({label: trans("responsiveLayout.rowDefinition"), tooltip: trans("responsiveLayout.rowsDefinitionTooltip")})} + {!children.useFlexLayout.getView() && children.templateColumns.propertyView({ + label: trans("responsiveLayout.columnDefinition"), + tooltip: trans("responsiveLayout.columnsDefinitionTooltip") + })} + {!children.useFlexLayout.getView() && children.templateRows.propertyView({ + label: trans("responsiveLayout.rowDefinition"), + tooltip: trans("responsiveLayout.rowsDefinitionTooltip") + })} {children.columnGap.propertyView({label: trans("responsiveLayout.columnGap")})} {children.rowGap.propertyView({label: trans("responsiveLayout.rowGap")})}
diff --git a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx index e32c32c09..8e25d053d 100644 --- a/client/packages/lowcoder/src/comps/controls/optionsControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/optionsControl.tsx @@ -40,6 +40,7 @@ import { ControlItemCompBuilder } from "comps/generators/controlCompBuilder"; import { ColorControl } from "./colorControl"; import { StringStateControl } from "./codeStateControl"; import { reduceInContext } from "../utils/reduceContext"; +import React from "react"; const OptionTypes = [ { @@ -65,10 +66,13 @@ type OptionControlParam = { title?: string; // The new option's label name newOptionLabel?: string; + // Whether to use flex layout (for column options) + useFlexLayout?: boolean; }; type OptionPropertyParam = { autoMap?: boolean; + useFlexLayout?: boolean; }; interface OptionCompProperty { @@ -176,7 +180,7 @@ export function manualOptionsControl( itemTitle={(comp) => comp.children.label.getView()} popoverTitle={() => trans("edit")} content={(comp) => { - return hasPropertyView(comp) ? comp.propertyView({}) : comp.getPropertyView(); + return hasPropertyView(comp) ? comp.propertyView({ useFlexLayout: param.useFlexLayout }) : comp.getPropertyView(); }} items={manualComp.getView()} onAdd={() => { @@ -576,12 +580,13 @@ const StyledContent = styled.div` } `; -const ColumnOption = new MultiCompBuilder( +let ColumnOption = new MultiCompBuilder( { id: valueComp(-1), label: StringControl, key: StringControl, minWidth: withDefault(RadiusControl, ""), + width: withDefault(RadiusControl, ""), background: withDefault(ColorControl, ""), backgroundImage: withDefault(StringControl, ""), border: withDefault(ColorControl, ""), @@ -590,48 +595,55 @@ const ColumnOption = new MultiCompBuilder( padding: withDefault(StringControl, ""), }, (props) => props -) -.setPropertyViewFn((children) => ( - - {children.minWidth.propertyView({ - label: trans('responsiveLayout.minWidth'), - preInputNode: , - placeholder: '3px', - })} - {children.background.propertyView({ - label: trans('style.background'), - })} - {children.backgroundImage.propertyView({ - label: `Background Image`, - // preInputNode: , - placeholder: 'https://temp.im/350x400', - })} - {children.border.propertyView({ - label: trans('style.border') - })} - {children.radius.propertyView({ - label: trans('style.borderRadius'), - preInputNode: , - placeholder: '3px', - })} - {children.margin.propertyView({ - label: trans('style.margin'), - preInputNode: , - placeholder: '3px', - })} - {children.padding.propertyView({ - label: trans('style.padding'), - preInputNode: , - placeholder: '3px', - })} - -)) - .build(); +).build(); + +// Add propertyView method through class extension +ColumnOption = class extends ColumnOption implements OptionCompProperty { + propertyView(param: OptionPropertyParam) { + const useFlexLayout = param?.useFlexLayout !== undefined ? param.useFlexLayout : true; + + return ( + + {useFlexLayout && this.children.width.propertyView({ + label: trans('responsiveLayout.width'), + preInputNode: , + placeholder: '50%', + })} + {this.children.background.propertyView({ + label: trans('style.background'), + })} + {this.children.backgroundImage.propertyView({ + label: `Background Image`, + // preInputNode: , + placeholder: 'https://temp.im/350x400', + })} + {this.children.border.propertyView({ + label: trans('style.border') + })} + {this.children.radius.propertyView({ + label: trans('style.borderRadius'), + preInputNode: , + placeholder: '3px', + })} + {this.children.margin.propertyView({ + label: trans('style.margin'), + preInputNode: , + placeholder: '3px', + })} + {this.children.padding.propertyView({ + label: trans('style.padding'), + preInputNode: , + placeholder: '3px', + })} + + ); + } +}; export const ColumnOptionControl = manualOptionsControl(ColumnOption, { initOptions: [ - { id: 0, key: "Column1", label: "Column1" }, - { id: 1, key: "Column2", label: "Column2" }, + { id: 0, key: "Column1", label: "Column1", width: "" }, + { id: 1, key: "Column2", label: "Column2", width: "" }, ], uniqField: "key", autoIncField: "id", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index d1eff10d7..ba248eea4 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -3910,6 +3910,8 @@ export const en = { "rowStyle": "Row Style", "columnStyle": "Column Style", "minWidth": "Min. Width", + "width": "Width", + "widthTooltip": "Set the column width (e.g., '300px', '50%', '100%'). When set to 100%, columns will stack vertically.", "rowBreak": "Row Break", "useComponentWidth" : "Use Self Size", "useComponentWidthDesc" : "Use the container width instead the App width", @@ -3918,6 +3920,8 @@ export const en = { "columnsLayout": "Columns Layout", "columnsDefinitionTooltip": "Columns can be defined freely based on the CSS columns properties. For example, 'auto auto' will create two columns with equal width. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-columns", "rowsDefinitionTooltip": "Rows can be defined freely based on the CSS rows properties. For example, 'auto auto' will create two rows with equal height. Read more here: https://css-tricks.com/almanac/properties/g/grid-template-rows", + "useFlexLayout": "Use Flexible Layout", + "useFlexLayoutTooltip": "Enable responsive behavior where columns can wrap when there's not enough space" }, "splitLayout" : { "column": "View Areas",