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 (
+
+
+ {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
+ }
+}