diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index 9704e75a71cb..2d5107fdbdf3 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -1,4 +1,5 @@ /** @jsxRuntime classic */ +/** @jsxFrag React.Fragment */ /** @jsx jsx */ import { jsx } from '@emotion/react' import React from 'react' @@ -19,8 +20,16 @@ import { MetadataUtils } from '../../core/model/element-metadata-utils' import { strictEvery } from '../../core/shared/array-utils' import { useDispatch } from '../editor/store/dispatch-context' import type { DetectedLayoutSystem } from 'utopia-shared/src/types' -import { NO_OP } from '../../core/shared/utils' -import { Icons, NumberInput, SquareButton, Subdued } from '../../uuiui' +import { NO_OP, assertNever } from '../../core/shared/utils' +import { + FlexRow, + Icons, + InspectorSectionIcons, + NumberInput, + SquareButton, + Subdued, + Tooltip, +} from '../../uuiui' import type { CSSKeyword, CSSNumber, @@ -41,7 +50,7 @@ import { isValidGridDimensionKeyword, type GridDimension, } from './common/css-utils' -import { applyCommandsAction } from '../editor/actions/action-creators' +import { applyCommandsAction, transientActions } from '../editor/actions/action-creators' import type { PropertyToUpdate } from '../canvas/commands/set-property-command' import { propertyToDelete, @@ -66,6 +75,8 @@ import type { DropdownMenuItem } from '../../uuiui/radix-components' import { DropdownMenu, regularDropdownMenuItem } from '../../uuiui/radix-components' import { useInspectorLayoutInfo } from './common/property-path-hooks' import { NumberOrKeywordControl } from '../../uuiui/inputs/number-or-keyword-control' +import { cssNumberEqual } from '../canvas/controls/select-mode/controls-common' +import type { EditorAction } from '../editor/action-types' const axisDropdownMenuButton = 'axisDropdownMenuButton' @@ -587,6 +598,12 @@ function sanitizeAreaName(areaName: string): string { return areaName.replace(reAlphanumericDashUnderscore, '-') } +function serializeValue(v: CSSNumber) { + return v.unit == null || v.unit === 'px' ? v.value : cssNumberToString(v) +} + +type GridGapControlSplitState = 'unified' | 'split' + const GapRowColumnControl = React.memo(() => { const dispatch = useDispatch() @@ -612,17 +629,30 @@ const GapRowColumnControl = React.memo(() => { const columnGap = useInspectorLayoutInfo('columnGap') const rowGap = useInspectorLayoutInfo('rowGap') - const onSubmit = React.useCallback( + const [controlSplitState, setControlSplitState] = React.useState( + cssNumberEqual(columnGap.value, rowGap.value) ? 'unified' : 'split', + ) + + const cycleControlSplitState = React.useCallback(() => { + setControlSplitState((state) => { + switch (state) { + case 'split': + return 'unified' + case 'unified': + return 'split' + default: + assertNever(state) + } + }) + }, []) + + const onSubmitSplitValue = React.useCallback( (target: 'columnGap' | 'rowGap') => (value: UnknownOrEmptyInput, transient?: boolean) => { if (grid == null) { return } - function serializeValue(v: CSSNumber) { - return v.unit == null || v.unit === 'px' ? v.value : cssNumberToString(v) - } - if (isCSSNumber(value)) { let updates: PropertyToUpdate[] = [] @@ -665,41 +695,117 @@ const GapRowColumnControl = React.memo(() => { [dispatch, grid, gap, rowGap, columnGap], ) + const onSubmitUnifiedValue = React.useCallback( + (value: UnknownOrEmptyInput, transient?: boolean) => { + if (grid == null || !isCSSNumber(value)) { + return + } + + const transientWrapper = (actions: EditorAction[]) => + transient ? [transientActions(actions)] : actions + + dispatch( + transientWrapper([ + applyCommandsAction([ + updateBulkProperties('always', grid.elementPath, [ + propertyToDelete(PP.create('style', 'columnGap')), + propertyToDelete(PP.create('style', 'rowGap')), + propertyToDelete(PP.create('style', 'gap')), + propertyToSet(PP.create('style', 'gridGap'), serializeValue(value)), + ]), + ]), + ]), + ) + }, + [dispatch, grid], + ) + + const tooltipTitle = + controlSplitState === 'unified' + ? 'Longhand gap' + : controlSplitState === 'split' + ? 'Shorthand gap' + : assertNever(controlSplitState) + + const modeIcon = React.useMemo(() => { + switch (controlSplitState) { + case 'split': + return + case 'unified': + return + default: + assertNever(controlSplitState) + } + }, [controlSplitState]) + if (grid == null) { return null } return ( - - - - + + {when( + controlSplitState === 'unified', + + + , + )} + {when( + controlSplitState === 'split', + + + + , + )} + + + {modeIcon} + + + ) }) GapRowColumnControl.displayName = 'GapRowColumnControl'