diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts index d02379c7bb14..a633677b2798 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts @@ -380,7 +380,7 @@ function asMaybeNamedAreaOrValue( value: number | string | null, ): string | number { if (value == null) { - return 0 + return 1 } else if (typeof value === 'number') { const template = axis === 'row' ? grid.gridTemplateRows : grid.gridTemplateColumns if (template?.type === 'DIMENSIONS') { @@ -389,6 +389,7 @@ function asMaybeNamedAreaOrValue( return maybeAreaStart.areaName } } + return value === 0 ? 1 : value } return value } diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index b9967f66ee7a..342df8455e71 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -1,3 +1,6 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@emotion/react' import React from 'react' import { createSelector } from 'reselect' import { when } from '../../utils/react-conditionals' @@ -23,7 +26,24 @@ import { import { executeFirstApplicableStrategy } from './inspector-strategies/inspector-strategy' import type { DetectedLayoutSystem } from 'utopia-shared/src/types' import { assertNever } from '../../core/shared/utils' -import { Subdued } from '../../uuiui' +import { Icons, NumberInput, SquareButton, Subdued } from '../../uuiui' +import type { CSSNumber, GridCSSNumberUnit, UnknownOrEmptyInput } from './common/css-utils' +import { gridCSSNumber, isCSSNumber, type GridCSSNumber } from './common/css-utils' +import { applyCommandsAction } from '../editor/actions/action-creators' +import { setProperty } from '../canvas/commands/set-property-command' +import * as PP from '../../core/shared/property-path' +import type { + GridAutoOrTemplateBase, + GridContainerProperties, + GridPosition, +} from '../../core/shared/element-template' +import { + gridPositionValue, + type ElementInstanceMetadata, + type GridElementProperties, +} from '../../core/shared/element-template' +import { setGridPropsCommands } from '../canvas/canvas-strategies/strategies/grid-helpers' +import { type CanvasCommand } from '../canvas/commands/commands' export const layoutSystemSelector = createSelector( metadataSelector, @@ -106,10 +126,38 @@ export const FlexSection = React.memo(() => { [addFlexLayoutSystem, addGridLayoutSystem], ) + const grid = useEditorState( + Substores.metadata, + (store) => + layoutSystem === 'grid' && store.editor.selectedViews.length === 1 + ? MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + store.editor.selectedViews[0], + ) + : null, + 'FlexSection grid', + ) + + const columns: GridCSSNumber[] = React.useMemo(() => { + return getGridTemplateAxisValues({ + calculated: grid?.specialSizeMeasurements.containerGridProperties.gridTemplateColumns ?? null, + fromProps: + grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns ?? null, + }) + }, [grid]) + + const rows: GridCSSNumber[] = React.useMemo(() => { + return getGridTemplateAxisValues({ + calculated: grid?.specialSizeMeasurements.containerGridProperties.gridTemplateRows ?? null, + fromProps: + grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows ?? null, + }) + }, [grid]) + return (
- + {when( layoutSystem === 'grid' || layoutSystem === 'flex', @@ -123,14 +171,24 @@ export const FlexSection = React.memo(() => { {when( layoutSystem === 'grid', '}> -
- Grid inspector coming soon... +
+ {grid != null ? ( + + + + + ) : null}
, )} {when( layoutSystem === 'flex', - <> + @@ -145,9 +203,260 @@ export const FlexSection = React.memo(() => { - , + , )}
) }) + +const TemplateDimensionControl = React.memo( + ({ + grid, + values, + axis, + title, + }: { + grid: ElementInstanceMetadata + values: GridCSSNumber[] + axis: 'column' | 'row' + title: string + }) => { + const dispatch = useDispatch() + + const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) + + const onUpdate = React.useCallback( + (index: number) => (value: UnknownOrEmptyInput) => { + if (!isCSSNumber(value)) { + return + } + + const newValues = [...values] + newValues[index] = gridCSSNumber( + value.value, + (value.unit as GridCSSNumberUnit) ?? values[index].unit, + values[index].areaName, + ) + + dispatch([ + applyCommandsAction([ + setProperty( + 'always', + grid.elementPath, + PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'), + gridNumbersToTemplateString(newValues), + ), + ]), + ]) + }, + [grid, values, dispatch, axis], + ) + + const onRemove = React.useCallback( + (index: number) => () => { + const newValues = values.filter((_, idx) => idx !== index) + + let commands: CanvasCommand[] = [ + setProperty( + 'always', + grid.elementPath, + PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'), + gridNumbersToTemplateString(newValues), + ), + ] + + // adjust the position of the elements if they need to be moved + const adjustedGridTemplate = removeTemplateValueAtIndex( + grid.specialSizeMeasurements.containerGridProperties, + axis, + index, + ) + + const gridIndex = index + 1 // grid boundaries are 1-based + + const children = MetadataUtils.getChildrenUnordered(metadataRef.current, grid.elementPath) + for (const child of children) { + let updated: Partial = { + ...child.specialSizeMeasurements.elementGridProperties, + } + + function needsAdjusting(pos: GridPosition | null, bound: number) { + return pos != null && + pos !== 'auto' && + pos.numericalPosition != null && + pos.numericalPosition >= bound + ? pos.numericalPosition + : null + } + + const position = child.specialSizeMeasurements.elementGridProperties + if (axis === 'column') { + const adjustColumnStart = needsAdjusting(position.gridColumnStart, gridIndex) + const adjustColumnEnd = needsAdjusting(position.gridColumnEnd, gridIndex + 1) + if (adjustColumnStart != null) { + updated.gridColumnStart = gridPositionValue(adjustColumnStart - 1) + } + if (adjustColumnEnd != null) { + updated.gridColumnEnd = gridPositionValue(adjustColumnEnd - 1) + } + } else { + const adjustRowStart = needsAdjusting(position.gridRowStart, gridIndex) + const adjustRowEnd = needsAdjusting(position.gridRowEnd, gridIndex + 1) + if (adjustRowStart != null) { + updated.gridRowStart = gridPositionValue(adjustRowStart - 1) + } + if (adjustRowEnd != null) { + updated.gridRowEnd = gridPositionValue(adjustRowEnd - 1) + } + } + + commands.push(...setGridPropsCommands(child.elementPath, adjustedGridTemplate, updated)) + } + + dispatch([applyCommandsAction(commands)]) + }, + [grid, values, dispatch, axis, metadataRef], + ) + + const onAdd = React.useCallback(() => { + const newValues = values.concat(gridCSSNumber(1, 'fr', null)) + dispatch([ + applyCommandsAction([ + setProperty( + 'always', + grid.elementPath, + PP.create('style', axis === 'column' ? 'gridTemplateColumns' : 'gridTemplateRows'), + gridNumbersToTemplateString(newValues), + ), + ]), + ]) + }, [dispatch, grid, axis, values]) + + return ( +
+
+
{title}
+ + + +
+ {values.map((col, index) => { + return ( +
.removeButton': { + visibility: 'hidden', + }, + ':hover': { + '& > .removeButton': { + visibility: 'visible', + }, + }, + }} + > +
+ + {col.areaName ?? index + 1} + + +
+ + {/* TODO replace this with a context menu! */} + + +
+ ) + })} +
+ ) + }, +) +TemplateDimensionControl.displayName = 'TemplateDimensionControl' + +function removeTemplateValueAtIndex( + original: GridContainerProperties, + axis: 'column' | 'row', + index: number, +): GridContainerProperties { + function removeDimension(dimensions: GridCSSNumber[]) { + return dimensions.filter((_, idx) => idx !== index) + } + + const gridTemplateRows = + axis === 'row' && original.gridTemplateRows?.type === 'DIMENSIONS' + ? { + ...original.gridTemplateRows, + dimensions: removeDimension(original.gridTemplateRows.dimensions), + } + : original.gridTemplateRows + + const gridTemplateColumns = + axis === 'column' && original.gridTemplateColumns?.type === 'DIMENSIONS' + ? { + ...original.gridTemplateColumns, + dimensions: removeDimension(original.gridTemplateColumns.dimensions), + } + : original.gridTemplateColumns + + return { + ...original, + gridTemplateRows: gridTemplateRows, + gridTemplateColumns: gridTemplateColumns, + } +} + +function gridNumbersToTemplateString(values: GridCSSNumber[]) { + return values + .map((v) => { + const areaName = v.areaName != null ? `[${v.areaName}] ` : '' + const unit = v.unit != null ? `${v.unit}` : '' + return areaName + `${v.value}` + unit + }) + .join(' ') +} + +function getGridTemplateAxisValues(template: { + calculated: GridAutoOrTemplateBase | null + fromProps: GridAutoOrTemplateBase | null +}): GridCSSNumber[] { + const { calculated, fromProps } = template + if (fromProps?.type !== 'DIMENSIONS' && calculated?.type !== 'DIMENSIONS') { + return [] + } + + const calculatedDimensions = calculated?.type === 'DIMENSIONS' ? calculated.dimensions : [] + const fromPropsDimensions = fromProps?.type === 'DIMENSIONS' ? fromProps.dimensions : [] + if (calculatedDimensions.length === 0) { + return fromPropsDimensions + } else if (calculatedDimensions.length === fromPropsDimensions.length) { + return fromPropsDimensions + } else { + return calculatedDimensions + } +}