diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts
index b48f81e170ca..feec3148dc00 100644
--- a/editor/src/components/inspector/common/css-utils.ts
+++ b/editor/src/components/inspector/common/css-utils.ts
@@ -652,6 +652,21 @@ export type GridCSSMinmax = BaseGridDimension & {
max: GridCSSNumber | GridCSSKeyword
}
+export function parseGridCSSMinmaxOrRepeat(input: string): GridCSSMinmax | GridCSSRepeat | null {
+ const parsed = csstree.parse(input, { context: 'value' })
+ if (parsed.type === 'Value') {
+ const parsedDimensions = parseGridChildren(parsed.children)
+ if (
+ isRight(parsedDimensions) &&
+ parsedDimensions.value.length === 1 &&
+ (isGridCSSMinmax(parsedDimensions.value[0]) || isGridCSSRepeat(parsedDimensions.value[0]))
+ ) {
+ return parsedDimensions.value[0]
+ }
+ }
+ return null
+}
+
export function isGridCSSKeyword(dim: GridDimension): dim is GridCSSKeyword {
return dim.type === 'KEYWORD'
}
@@ -1007,7 +1022,6 @@ const validGridDimensionKeywords = [
'subgrid',
'auto-fit',
'auto-fill',
- 'minmax', // TODO this should be removed once we have proper editing of grid template expressions!
] as const
export type ValidGridDimensionKeyword = (typeof validGridDimensionKeywords)[number]
diff --git a/editor/src/components/inspector/flex-section.spec.browser2.tsx b/editor/src/components/inspector/flex-section.spec.browser2.tsx
index f8ad05c41e96..1cdcc970fb6d 100644
--- a/editor/src/components/inspector/flex-section.spec.browser2.tsx
+++ b/editor/src/components/inspector/flex-section.spec.browser2.tsx
@@ -53,29 +53,19 @@ describe('flex section', () => {
const grid = await renderResult.renderedDOM.findByTestId('grid')
expect(grid.style.gridTemplateColumns).toEqual('[area1] auto 1fr 1fr 1fr 1fr')
})
- it('uses auto for defaults when empty', async () => {
- const renderResult = await renderTestEditorWithCode(
- gridProjectWithoutTemplate,
- 'await-first-dom-report',
- )
- await selectComponentsForTest(renderResult, [EP.fromString('sb/grid')])
- const control = await screen.findByTestId('grid-dimension-column-0')
- await typeIntoField(control, '100px')
- const grid = await renderResult.renderedDOM.findByTestId('grid')
- expect(grid.style.gridTemplateColumns).toEqual('100px auto auto')
- })
- it('updates a repeated value', async () => {
+ it('updates a repeat expression', async () => {
const renderResult = await renderTestEditorWithCode(
gridProjectWithRepeat,
'await-first-dom-report',
)
await selectComponentsForTest(renderResult, [EP.fromString('sb/grid')])
- await typeIntoField(await screen.findByTestId('grid-dimension-column-2'), '42')
const grid = await renderResult.renderedDOM.findByTestId('grid')
- expect(grid.style.gridTemplateColumns).toEqual('[area1] 1fr repeat(2, 10px 42px) 2fr')
- await typeIntoField(await screen.findByTestId('grid-dimension-column-1'), '.5fr')
+ await typeIntoField(
+ await screen.findByTestId('grid-dimension-column-1'),
+ 'repeat(2, 0.5fr 42px)',
+ )
expect(grid.style.gridTemplateColumns).toEqual('[area1] 1fr repeat(2, 0.5fr 42px) 2fr')
})
})
diff --git a/editor/src/components/inspector/flex-section.spec.tsx b/editor/src/components/inspector/flex-section.spec.tsx
deleted file mode 100644
index a2ffdf898ef5..000000000000
--- a/editor/src/components/inspector/flex-section.spec.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { cssKeyword, cssNumber, gridCSSKeyword, gridCSSNumber } from './common/css-utils'
-import { mergeGridTemplateValues } from './flex-section'
-
-describe('mergeGridTemplateValues', () => {
- it('empty values', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [],
- fromProps: [],
- autoValues: [],
- }),
- ).toEqual([])
- })
-
- it('only calculated', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [gridCSSNumber(cssNumber(1), null)],
- fromProps: [],
- autoValues: [],
- }),
- ).toEqual([gridCSSKeyword(cssKeyword('auto'), null)])
- })
-
- it('competing calculated and from props', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [gridCSSNumber(cssNumber(1), null)],
- fromProps: [gridCSSNumber(cssNumber(2), null)],
- autoValues: [],
- }),
- ).toEqual([gridCSSNumber(cssNumber(2), null)])
- })
-
- it('multiple elements, empty from props', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [gridCSSNumber(cssNumber(1), null), gridCSSNumber(cssNumber(2), null)],
- fromProps: [gridCSSNumber(cssNumber(2), null)],
- autoValues: [],
- }),
- ).toEqual([gridCSSNumber(cssNumber(2), null), gridCSSNumber(cssNumber(2), null)])
- })
-
- it('multiple elements, competing', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [gridCSSNumber(cssNumber(1), null), gridCSSNumber(cssNumber(2), null)],
- fromProps: [gridCSSNumber(cssNumber(2), null), gridCSSKeyword(cssKeyword('auto'), null)],
- autoValues: [],
- }),
- ).toEqual([gridCSSNumber(cssNumber(2), null), gridCSSKeyword(cssKeyword('auto'), null)])
- })
-
- it('one auto value, auto calculated', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [gridCSSKeyword(cssKeyword('auto'), null)],
- fromProps: [],
- autoValues: [gridCSSNumber(cssNumber(42), null)],
- }),
- ).toEqual([gridCSSNumber(cssNumber(42), null)])
- })
-
- it('one auto value, multiple auto calculated', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- ],
- fromProps: [],
- autoValues: [gridCSSNumber(cssNumber(42), null)],
- }),
- ).toEqual([
- gridCSSNumber(cssNumber(42), null),
- gridCSSNumber(cssNumber(42), null),
- gridCSSNumber(cssNumber(42), null),
- ])
- })
-
- it('multiple auto values, multiple auto calculated', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- ],
- fromProps: [],
- autoValues: [
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(2), null),
- gridCSSNumber(cssNumber(3), null),
- ],
- }),
- ).toEqual([
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(2), null),
- gridCSSNumber(cssNumber(3), null),
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(2), null),
- ])
- })
-
- it('one auto value, multiple auto calculated, but values from props', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- ],
- fromProps: [gridCSSNumber(cssNumber(42), null)],
- autoValues: [gridCSSNumber(cssNumber(1), null)],
- }),
- ).toEqual([
- gridCSSNumber(cssNumber(42), null),
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(1), null),
- gridCSSNumber(cssNumber(1), null),
- ])
- })
-
- it('multiple auto values, multiple auto calculated, but values from props', async () => {
- expect(
- mergeGridTemplateValues({
- calculated: [
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- ],
- fromProps: [
- gridCSSNumber(cssNumber(42), null),
- gridCSSKeyword(cssKeyword('auto'), null),
- gridCSSNumber(cssNumber(23), null),
- ],
- autoValues: [gridCSSNumber(cssNumber(1), null), gridCSSNumber(cssNumber(2), null)],
- }),
- ).toEqual([
- gridCSSNumber(cssNumber(42), null),
- gridCSSNumber(cssNumber(2), null),
- gridCSSNumber(cssNumber(23), null),
- gridCSSNumber(cssNumber(2), null),
- gridCSSNumber(cssNumber(1), null),
- ])
- })
-})
diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx
index 1d94ebbd1ada..3ff686593e13 100644
--- a/editor/src/components/inspector/flex-section.tsx
+++ b/editor/src/components/inspector/flex-section.tsx
@@ -4,7 +4,7 @@
import { jsx } from '@emotion/react'
import React from 'react'
import { createSelector } from 'reselect'
-import { when } from '../../utils/react-conditionals'
+import { unless, when } from '../../utils/react-conditionals'
import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook'
import { AddRemoveLayoutSystemControl } from './add-remove-layout-system-control'
import { FlexDirectionToggle } from './flex-direction-control'
@@ -35,8 +35,6 @@ import type {
CSSKeyword,
CSSNumber,
GridAutoFlow,
- GridCSSKeyword,
- GridDiscreteDimension,
UnknownOrEmptyInput,
ValidGridDimensionKeyword,
} from './common/css-utils'
@@ -50,10 +48,7 @@ import {
isCSSKeyword,
isCSSNumber,
isEmptyInputValue,
- isGridCSSKeyword,
isGridCSSNumber,
- isGridCSSRepeat,
- isValidGridDimensionKeyword,
printArrayGridDimensions,
type GridDimension,
} from './common/css-utils'
@@ -66,22 +61,13 @@ 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 type { GridContainerProperties, GridPosition } from '../../core/shared/element-template'
import {
gridPositionValue,
type ElementInstanceMetadata,
type GridElementProperties,
} from '../../core/shared/element-template'
-import {
- expandGridDimensions,
- removeGridTemplateDimensionAtIndex,
- replaceGridTemplateDimensionAtIndex,
- setGridPropsCommands,
-} from '../canvas/canvas-strategies/strategies/grid-helpers'
+import { setGridPropsCommands } from '../canvas/canvas-strategies/strategies/grid-helpers'
import { type CanvasCommand } from '../canvas/commands/commands'
import type { DropdownMenuItem } from '../../uuiui/radix-components'
import {
@@ -92,7 +78,6 @@ import {
separatorRadixSelectOption,
} from '../../uuiui/radix-components'
import { useInspectorLayoutInfo, useInspectorStyleInfo } from './common/property-path-hooks'
-import { NumberOrKeywordControl } from '../../uuiui/inputs/number-or-keyword-control'
import { optionalMap } from '../../core/shared/optional-utils'
import { cssNumberEqual } from '../canvas/controls/select-mode/controls-common'
import type { EditorAction } from '../editor/action-types'
@@ -104,6 +89,7 @@ import {
useSetHoveredControlsHandlers,
} from '../canvas/controls/select-mode/select-mode-hooks'
import type { Axis } from '../canvas/gap-utils'
+import { GridExpressionInput } from '../../uuiui/inputs/grid-expression-input'
const axisDropdownMenuButton = 'axisDropdownMenuButton'
@@ -160,44 +146,6 @@ export const FlexSection = React.memo(() => {
'FlexSection grid',
)
- const columns = React.useMemo((): GridDiscreteDimension[] => {
- const autoCols: GridDimension[] =
- grid?.specialSizeMeasurements.containerGridProperties.gridAutoColumns?.type === 'DIMENSIONS'
- ? grid.specialSizeMeasurements.containerGridProperties.gridAutoColumns.dimensions
- : []
-
- const merged = mergeGridTemplateValues({
- autoValues: autoCols,
- ...getGridTemplateAxisValues({
- calculated:
- grid?.specialSizeMeasurements.containerGridProperties.gridTemplateColumns ?? null,
- fromProps:
- grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateColumns ??
- null,
- }),
- })
-
- return merged.filter((v) => v.type !== 'REPEAT')
- }, [grid])
-
- const rows = React.useMemo((): GridDiscreteDimension[] => {
- const autoRows: GridDimension[] =
- grid?.specialSizeMeasurements.containerGridProperties.gridAutoRows?.type === 'DIMENSIONS'
- ? grid.specialSizeMeasurements.containerGridProperties.gridAutoRows.dimensions
- : []
-
- const merged = mergeGridTemplateValues({
- autoValues: autoRows,
- ...getGridTemplateAxisValues({
- calculated: grid?.specialSizeMeasurements.containerGridProperties.gridTemplateRows ?? null,
- fromProps:
- grid?.specialSizeMeasurements.containerGridPropertiesFromProps.gridTemplateRows ?? null,
- }),
- })
-
- return merged.filter((v) => v.type !== 'REPEAT')
- }, [grid])
-
return (
@@ -211,13 +159,8 @@ export const FlexSection = React.memo(() => {
) : null}
,
@@ -247,21 +190,13 @@ export const FlexSection = React.memo(() => {
)
})
-const gridDimensionDropdownKeywords = [
- { label: 'Auto', value: cssKeyword('auto') },
- { label: 'Min-Content', value: cssKeyword('min-content') },
- { label: 'Max-Content', value: cssKeyword('max-content') },
-]
-
const TemplateDimensionControl = React.memo(
({
grid,
- values,
axis,
title,
}: {
grid: ElementInstanceMetadata
- values: GridDiscreteDimension[]
axis: 'column' | 'row'
title: string
}) => {
@@ -269,6 +204,25 @@ const TemplateDimensionControl = React.memo(
const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata)
+ const values = React.useMemo((): GridDimension[] => {
+ // TODO: handle gridAutoRows/Cols too
+ switch (axis) {
+ case 'row': {
+ const { gridTemplateRows } = grid.specialSizeMeasurements.containerGridPropertiesFromProps
+
+ return gridTemplateRows?.type === 'DIMENSIONS' ? gridTemplateRows.dimensions : []
+ }
+ case 'column': {
+ const { gridTemplateColumns } =
+ grid.specialSizeMeasurements.containerGridPropertiesFromProps
+
+ return gridTemplateColumns?.type === 'DIMENSIONS' ? gridTemplateColumns.dimensions : []
+ }
+ default:
+ assertNever(axis)
+ }
+ }, [grid, axis])
+
const template = React.useMemo(() => {
const fromProps =
axis === 'column'
@@ -280,24 +234,15 @@ const TemplateDimensionControl = React.memo(
return fromProps
}, [grid, axis, values])
- const expandedTemplate = React.useMemo(() => {
- if (template?.type !== 'DIMENSIONS') {
- return []
- }
- return expandGridDimensions(template.dimensions)
- }, [template])
-
const onUpdateDimension = React.useCallback(
(index: number) => (newValue: GridDimension) => {
if (template?.type !== 'DIMENSIONS') {
return
}
- const newDimensions = replaceGridTemplateDimensionAtIndex(
- template.dimensions,
- expandedTemplate,
- index,
- newValue,
- )
+ const left = template.dimensions.slice(0, index)
+ const right = template.dimensions.slice(index + 1)
+
+ const newDimensions = [...left, newValue, ...right]
dispatch([
applyCommandsAction([
@@ -310,7 +255,7 @@ const TemplateDimensionControl = React.memo(
]),
])
},
- [template, expandedTemplate, dispatch, axis, grid],
+ [template, dispatch, axis, grid],
)
const onUpdateNumberOrKeyword = React.useCallback(
@@ -350,11 +295,10 @@ const TemplateDimensionControl = React.memo(
return
}
- const newValues = removeGridTemplateDimensionAtIndex(
- template.dimensions,
- expandedTemplate,
- index,
- )
+ const left = template.dimensions.slice(0, index)
+ const right = template.dimensions.slice(index + 1)
+
+ const newValues = [...left, ...right]
let commands: CanvasCommand[] = [
setProperty(
@@ -367,7 +311,7 @@ const TemplateDimensionControl = React.memo(
// adjust the position of the elements if they need to be moved
const adjustedGridTemplate = removeTemplateValueAtIndex(
- grid.specialSizeMeasurements.containerGridProperties,
+ grid.specialSizeMeasurements.containerGridPropertiesFromProps,
axis,
index,
)
@@ -377,7 +321,7 @@ const TemplateDimensionControl = React.memo(
const children = MetadataUtils.getChildrenUnordered(metadataRef.current, grid.elementPath)
for (const child of children) {
let updated: Partial
= {
- ...child.specialSizeMeasurements.elementGridProperties,
+ ...child.specialSizeMeasurements.elementGridPropertiesFromProps,
}
function needsAdjusting(pos: GridPosition | null, bound: number) {
@@ -389,7 +333,7 @@ const TemplateDimensionControl = React.memo(
: null
}
- const position = child.specialSizeMeasurements.elementGridProperties
+ const position = child.specialSizeMeasurements.elementGridPropertiesFromProps
if (axis === 'column') {
const adjustColumnStart = needsAdjusting(position.gridColumnStart, gridIndex)
const adjustColumnEnd = needsAdjusting(position.gridColumnEnd, gridIndex + 1)
@@ -415,7 +359,7 @@ const TemplateDimensionControl = React.memo(
dispatch([applyCommandsAction(commands)])
},
- [grid, dispatch, axis, metadataRef, template, expandedTemplate],
+ [grid, dispatch, axis, metadataRef, template],
)
const onAppend = React.useCallback(() => {
@@ -442,7 +386,7 @@ const TemplateDimensionControl = React.memo(
if (template?.type !== 'DIMENSIONS') {
return
}
- const container = grid.specialSizeMeasurements.containerGridProperties
+ const container = grid.specialSizeMeasurements.containerGridPropertiesFromProps
const dimensions =
axis === 'column' ? container.gridTemplateColumns : container.gridTemplateRows
if (dimensions?.type !== 'DIMENSIONS') {
@@ -458,12 +402,14 @@ const TemplateDimensionControl = React.memo(
const newAreaName: string | null =
rawNewAreaName.length === 0 ? null : sanitizeAreaName(rawNewAreaName)
- const newValues = replaceGridTemplateDimensionAtIndex(
- template.dimensions,
- expandedTemplate,
- index,
- { ...values[index], areaName: newAreaName },
- )
+ const left = template.dimensions.slice(0, index)
+ const right = template.dimensions.slice(index + 1)
+
+ const newValues = [
+ ...left,
+ { ...values[index], areaName: newAreaName } as GridDimension,
+ ...right,
+ ]
let commands: CanvasCommand[] = [
setProperty(
@@ -488,14 +434,14 @@ const TemplateDimensionControl = React.memo(
...setGridPropsCommands(
child.elementPath,
adjustedGridTemplate,
- child.specialSizeMeasurements.elementGridProperties,
+ child.specialSizeMeasurements.elementGridPropertiesFromProps,
),
)
}
dispatch([applyCommandsAction(commands)])
},
- [grid, axis, values, dispatch, metadataRef, template, expandedTemplate],
+ [grid, axis, values, dispatch, metadataRef, template],
)
const dropdownMenuItems = React.useCallback(
@@ -531,6 +477,8 @@ const TemplateDimensionControl = React.memo(
[],
)
+ const dimensionsWithGeneratedIndexes = useGeneratedIndexesFromGridDimensions(values)
+
return (
- {values.map((value, index) => (
+ {dimensionsWithGeneratedIndexes.map((value, index) => (
{
setIsOpen(isDropdownOpen)
}, [])
+
+ const title = React.useMemo(() => {
+ if (indexFrom === indexTo) {
+ return value.areaName ?? indexFrom
+ }
+ return value.areaName ?? `${indexFrom} → ${indexTo}`
+ }, [value, indexFrom, indexTo])
+
+ const gridExpressionInputFocused = useGridExpressionInputFocused()
+
return (
- {value.areaName ?? index + 1}
+ {title}
-
-
-
-
+ {unless(
+ gridExpressionInputFocused.focused,
+
+
+ ,
+ )}
)
}
@@ -702,20 +667,6 @@ function renameAreaInTemplateAtIndex(
}
}
-function getGridTemplateAxisValues(template: {
- calculated: GridAutoOrTemplateBase | null
- fromProps: GridAutoOrTemplateBase | null
-}): { calculated: GridDimension[]; fromProps: GridDimension[] } {
- const { calculated, fromProps } = template
- if (fromProps?.type !== 'DIMENSIONS' && calculated?.type !== 'DIMENSIONS') {
- return { calculated: [], fromProps: [] }
- }
-
- const calculatedDimensions = calculated?.type === 'DIMENSIONS' ? calculated.dimensions : []
- const fromPropsDimensions = fromProps?.type === 'DIMENSIONS' ? fromProps.dimensions : []
- return { calculated: calculatedDimensions, fromProps: fromPropsDimensions }
-}
-
const reAlphanumericDashUnderscore = /[^0-9a-z\-_]+/gi
function sanitizeAreaName(areaName: string): string {
@@ -1086,49 +1037,45 @@ const AutoFlowControl = React.memo(() => {
})
AutoFlowControl.displayName = 'AutoFlowControl'
-export function mergeGridTemplateValues({
- calculated,
- fromProps,
- autoValues,
-}: {
- calculated: GridDimension[]
- fromProps: GridDimension[]
- autoValues: GridDimension[]
-}): GridDimension[] {
- const expanded = fromProps.flatMap((v) => {
- if (!isGridCSSRepeat(v)) {
- return v
- }
- if (isCSSKeyword(v.times)) {
- // NOTE: this will be removed soon with https://github.com/concrete-utopia/utopia/pull/6396, so no need to go in detail here
- return v
- }
- return Array(v.times).fill(v.value).flat()
- })
-
- function getExplicitValue(dimension: GridDimension, index: number): GridDimension {
- if (expanded.length === 0) {
- return gridCSSKeyword(cssKeyword('auto'), dimension.areaName)
- } else if (expanded[index] == null) {
- return dimension
- } else {
- return expanded[index]
- }
- }
-
- return calculated.map((c, index) => {
- const explicitValue = getExplicitValue(c, index)
-
- const autoValueIndex = index % autoValues.length // wrap around
- const autoValue = autoValues.at(autoValueIndex)
+interface GridDimensionWithGeneratedIndexes {
+ generatedIndexFrom: number
+ generatedIndexTo: number
+ dimension: GridDimension
+}
- if (isGridCSSKeyword(explicitValue) && explicitValue.value.value === 'auto') {
- return {
- ...(autoValue ?? explicitValue),
- areaName: explicitValue.areaName ?? autoValue?.areaName ?? null,
- } as GridCSSKeyword
- }
+function useGeneratedIndexesFromGridDimensions(
+ dimensions: Array,
+): Array {
+ return React.useMemo(() => {
+ let nextIndexFrom = 1
+ let result: Array = []
+
+ dimensions.forEach((dim) => {
+ if (dim.type !== 'REPEAT') {
+ result.push({
+ generatedIndexFrom: nextIndexFrom,
+ generatedIndexTo: nextIndexFrom,
+ dimension: dim,
+ })
+ nextIndexFrom += 1
+ } else {
+ // TODO: handle auto-fill and auto-fit in value.times
+ const shift = !isCSSKeyword(dim.times) ? dim.times * dim.value.length : dim.value.length
+ result.push({
+ generatedIndexFrom: nextIndexFrom,
+ generatedIndexTo: nextIndexFrom + shift - 1,
+ dimension: dim,
+ })
+ nextIndexFrom += shift
+ }
+ })
+ return result
+ }, [dimensions])
+}
- return explicitValue
- })
+const useGridExpressionInputFocused = () => {
+ const [focused, setFocused] = React.useState(false)
+ const onFocus = React.useCallback(() => setFocused(true), [])
+ const onBlur = React.useCallback(() => setFocused(false), [])
+ return { focused, onFocus, onBlur }
}
diff --git a/editor/src/uuiui/inputs/grid-expression-input.tsx b/editor/src/uuiui/inputs/grid-expression-input.tsx
new file mode 100644
index 000000000000..d3c0217663f9
--- /dev/null
+++ b/editor/src/uuiui/inputs/grid-expression-input.tsx
@@ -0,0 +1,87 @@
+import React from 'react'
+import {
+ cssKeyword,
+ isGridCSSNumber,
+ isValidGridDimensionKeyword,
+ parseCSSNumber,
+ parseGridCSSMinmaxOrRepeat,
+ printGridDimension,
+ type CSSKeyword,
+ type CSSNumber,
+ type GridDimension,
+ type ValidGridDimensionKeyword,
+} from '../../components/inspector/common/css-utils'
+import { isRight } from '../../core/shared/either'
+import { StringInput } from './string-input'
+
+interface GridExpressionInputProps {
+ testId: string
+ value: GridDimension
+ onUpdateNumberOrKeyword: (v: CSSNumber | CSSKeyword) => void
+ onUpdateDimension: (v: GridDimension) => void
+ onFocus: () => void
+ onBlur: () => void
+}
+
+export const GridExpressionInput = React.memo(
+ ({
+ testId,
+ value,
+ onUpdateNumberOrKeyword,
+ onUpdateDimension,
+ onFocus,
+ onBlur,
+ }: GridExpressionInputProps) => {
+ const [printValue, setPrintValue] = React.useState(printGridDimension(value))
+ React.useEffect(() => setPrintValue(printGridDimension(value)), [value])
+
+ const onChange = React.useCallback((e: React.ChangeEvent) => {
+ setPrintValue(e.target.value)
+ }, [])
+
+ const onKeyDown = React.useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key !== 'Enter') {
+ return
+ }
+ if (isValidGridDimensionKeyword(printValue)) {
+ return onUpdateNumberOrKeyword(cssKeyword(printValue))
+ }
+
+ const defaultUnit = isGridCSSNumber(value) ? value.value.unit : 'px'
+ const maybeNumber = parseCSSNumber(printValue, 'AnyValid', defaultUnit)
+ if (isRight(maybeNumber)) {
+ return onUpdateNumberOrKeyword(maybeNumber.value)
+ }
+
+ const maybeMinmax = parseGridCSSMinmaxOrRepeat(printValue)
+ if (maybeMinmax != null) {
+ return onUpdateDimension({
+ ...maybeMinmax,
+ areaName: value.areaName,
+ } as GridDimension)
+ }
+
+ if (printValue === '') {
+ return onUpdateNumberOrKeyword(cssKeyword('auto'))
+ }
+
+ setPrintValue(printGridDimension(value))
+ },
+ [printValue, onUpdateNumberOrKeyword, onUpdateDimension, value],
+ )
+
+ return (
+
+ )
+ },
+)
+GridExpressionInput.displayName = 'GridExpressionInput'
diff --git a/editor/src/uuiui/inputs/string-input.tsx b/editor/src/uuiui/inputs/string-input.tsx
index e6f0b3b2b23e..c38c162bb491 100644
--- a/editor/src/uuiui/inputs/string-input.tsx
+++ b/editor/src/uuiui/inputs/string-input.tsx
@@ -183,7 +183,7 @@ export const HeadlessStringInput = React.forwardRef(null)
@@ -237,6 +237,7 @@ export const HeadlessStringInput = React.forwardRef