diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx
index ac49bea9ee31..8e98bcb3f3b2 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx
@@ -2,9 +2,17 @@ import { fromString } from '../../../../core/shared/element-path'
import type { CanvasVector, Size, WindowPoint } from '../../../../core/shared/math-utils'
import { canvasVector, size, windowPoint } from '../../../../core/shared/math-utils'
import { assertNever } from '../../../../core/shared/utils'
+import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config'
+import { createModifiedProject } from '../../../../sample-projects/sample-project-utils.test-utils'
import type { Modifiers } from '../../../../utils/modifiers'
import { cmdModifier, emptyModifiers } from '../../../../utils/modifiers'
+import {
+ selectComponentsForTest,
+ setFeatureForBrowserTestsUseInDescribeBlockOnly,
+ wait,
+} from '../../../../utils/utils.test-utils'
import { selectComponents, setFocusedElement } from '../../../editor/actions/action-creators'
+import { StoryboardFilePath } from '../../../editor/store/editor-state'
import type { BorderRadiusCorner } from '../../border-radius-control-utils'
import { BorderRadiusCorners } from '../../border-radius-control-utils'
import type { EdgePosition } from '../../canvas-types'
@@ -23,6 +31,7 @@ import {
renderTestEditorWithCode,
makeTestProjectCodeWithSnippet,
getPrintedUiJsCode,
+ renderTestEditorWithModel,
} from '../../ui-jsx.test-utils'
describe('set border radius strategy', () => {
@@ -424,6 +433,333 @@ describe('set border radius strategy', () => {
})
})
})
+
+ describe('Tailwind', () => {
+ setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true)
+
+ const TailwindProject = (classes: string) =>
+ createModifiedProject({
+ [StoryboardFilePath]: `
+ import React from 'react'
+ import { Scene, Storyboard } from 'utopia-api'
+ export var storyboard = (
+
+
+
+
+
+ )
+
+ `,
+ [TailwindConfigPath]: `
+ const TailwindConfig = { }
+ export default TailwindConfig
+ `,
+ 'app.css': `
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;`,
+ })
+
+ it('border radius controls show up for elements that have tailwind border radius set', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await selectComponentsForTest(editor, [fromString('sb/scene/mydiv')])
+
+ const borderRadiusControls = BorderRadiusCorners.flatMap((corner) =>
+ editor.renderedDOM.queryAllByTestId(CircularHandleTestId(corner)),
+ )
+
+ expect(borderRadiusControls.length).toEqual(4)
+ })
+
+ it('border radius controls show up for elements that dont have tailwind border radius set', async () => {
+ const editor = await renderTestEditorWithModel(TailwindProject(''), 'await-first-dom-report')
+ await selectComponentsForTest(editor, [fromString('sb/scene/mydiv')])
+
+ const borderRadiusControls = BorderRadiusCorners.flatMap((corner) =>
+ editor.renderedDOM.queryAllByTestId(CircularHandleTestId(corner)),
+ )
+
+ expect(borderRadiusControls.length).toEqual(4)
+ })
+
+ describe('adjust border radius via handles', () => {
+ it('top left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
+ )
+ })
+
+ it('top right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tr', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
+ )
+ })
+
+ it('bottom left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
+ )
+ })
+
+ it('bottom right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
+ )
+ })
+ })
+ describe('adjust border radius via handles with non-arbitrary tailwind classes', () => {
+ it('top left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
+ )
+ })
+
+ it('top right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tr', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
+ )
+ })
+
+ it('bottom left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
+ )
+ })
+
+ it('bottom right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
+ )
+ })
+ })
+ describe('adjust border radius via handles, individually', () => {
+ it('top left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[22px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px] overflow-hidden',
+ )
+ })
+
+ it('top right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tr', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[22px] rounded-br-[10px] rounded-bl-[10px] overflow-hidden',
+ )
+ })
+
+ it('bottom left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'bl', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[22px] overflow-hidden',
+ )
+ })
+
+ it('bottom right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'br', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[10px] rounded-br-[22px] rounded-bl-[10px] overflow-hidden',
+ )
+ })
+ })
+ describe('adjust border radius via handles, individually, with non-arbitrary tailwind classes', () => {
+ it('top left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[2rem] rounded-tr-2xl rounded-br-2xl rounded-bl-2xl overflow-hidden',
+ )
+ })
+
+ it('top right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tr', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-[2rem] rounded-br-2xl rounded-bl-2xl overflow-hidden',
+ )
+ })
+
+ it('bottom left', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'bl', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-2xl rounded-br-2xl rounded-bl-[2rem] overflow-hidden',
+ )
+ })
+
+ it('bottom right', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-2xl'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'br', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-2xl rounded-br-[2rem] rounded-bl-2xl overflow-hidden',
+ )
+ })
+ })
+
+ describe('Overflow property handling', () => {
+ it('does not overwrite existing overflow property', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px] overflow-visible'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, cmdModifier)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute overflow-visible rounded-tl-[22px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px]',
+ )
+ })
+
+ it('shows toast message', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject('rounded-[10px]'),
+ 'await-first-dom-report',
+ )
+ await doDragTest(editor, 'tl', 10, cmdModifier)
+ expect(editor.getEditorState().editor.toasts).toHaveLength(1)
+ expect(editor.getEditorState().editor.toasts[0]).toEqual({
+ id: 'property-added',
+ level: 'NOTICE',
+ message: 'Element now hides overflowing content',
+ persistent: false,
+ })
+ })
+ })
+
+ it('can handle 4-value syntax', async () => {
+ const editor = await renderTestEditorWithModel(
+ TailwindProject(
+ 'rounded-tl-[14px] rounded-tr-[15px] rounded-br-[16px] rounded-bl-[17px] overflow-visible',
+ ),
+ 'await-first-dom-report',
+ )
+
+ await doDragTest(editor, 'tl', 10, emptyModifiers)
+ await editor.getDispatchFollowUpActionsFinished()
+ const div = editor.renderedDOM.getByTestId('mydiv')
+ expect(div.className).toEqual(
+ 'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[24px] rounded-tr-[15px] rounded-br-[16px] rounded-bl-[17px] overflow-visible',
+ )
+ })
+ })
})
function codeForDragTest(borderRadius: string): string {
diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx
index c2e84920c45c..9d9e18ff297a 100644
--- a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx
+++ b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx
@@ -1,18 +1,13 @@
import { styleStringInArray } from '../../../../utils/common-constants'
import type { Sides } from 'utopia-api/core'
-import { getLayoutProperty } from '../../../../core/layout/getLayoutProperty'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
-import { defaultEither, foldEither, right } from '../../../../core/shared/either'
-import type {
- ElementInstanceMetadata,
- JSXAttributes,
-} from '../../../../core/shared/element-template'
+import { foldEither } from '../../../../core/shared/either'
+import type { ElementInstanceMetadata } from '../../../../core/shared/element-template'
import {
isIntrinsicElement,
isJSXElement,
jsxElementName,
jsxElementNameEquals,
- modifiableAttributeIsAttributeNotFound,
} from '../../../../core/shared/element-template'
import type { CanvasPoint, CanvasVector, Size } from '../../../../core/shared/math-utils'
import {
@@ -32,7 +27,7 @@ import type {
CSSNumber,
ParsedCSSPropertiesKeys,
} from '../../../inspector/common/css-utils'
-import { cssNumber, ParsedCSSProperties, printCSSNumber } from '../../../inspector/common/css-utils'
+import { cssNumber, printCSSNumber } from '../../../inspector/common/css-utils'
import { stylePropPathMappingFn } from '../../../inspector/common/property-path-hooks'
import type {
BorderRadiusAdjustMode,
@@ -43,7 +38,7 @@ import {
BorderRadiusControlMinimumForDisplay,
maxBorderRadius,
} from '../../border-radius-control-utils'
-import { CSSCursor } from '../../canvas-types'
+import { CSSCursor, maybePropertyValue, type StyleInfo } from '../../canvas-types'
import type { CanvasCommand } from '../../commands/commands'
import { setCursorCommand } from '../../commands/set-cursor-command'
@@ -58,7 +53,6 @@ import {
measurementBasedOnOtherMeasurement,
precisionFromModifiers,
shouldShowControls,
- unitlessCSSNumberWithRenderedValue,
} from '../../controls/select-mode/controls-common'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
@@ -70,11 +64,6 @@ import {
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'
import { deleteProperties } from '../../commands/delete-properties-command'
-import { allElemsEqual } from '../../../../core/shared/array-utils'
-import * as PP from '../../../../core/shared/property-path'
-import { withUnderlyingTarget } from '../../../editor/store/editor-state'
-import type { ProjectContentTreeRoot } from '../../../assets'
-import { getModifiableJSXAttributeAtPath } from '../../../../core/shared/jsx-attribute-utils'
import { showToastCommand } from '../../commands/show-toast-command'
import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command'
@@ -112,7 +101,10 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = (
return null
}
- const borderRadius = borderRadiusFromElement(element)
+ const borderRadius = borderRadiusFromElement(
+ element,
+ canvasState.styleInfoReader(selectedElement),
+ )
if (borderRadius == null) {
return null
}
@@ -159,7 +151,10 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = (
[
setCursorCommand(CSSCursor.Radius),
...commands(selectedElement),
- ...getAddOverflowHiddenCommands(selectedElement, canvasState.projectContents),
+ ...getAddOverflowHiddenCommands(
+ selectedElement,
+ canvasState.styleInfoReader(selectedElement),
+ ),
setActiveFrames(
selectedElements.map((path) => ({
action: 'set-radius',
@@ -222,6 +217,7 @@ interface BorderRadiusData {
export function borderRadiusFromElement(
element: ElementInstanceMetadata,
+ styleInfo: StyleInfo | null,
): BorderRadiusData | null {
return foldEither(
() => null,
@@ -232,7 +228,7 @@ export function borderRadiusFromElement(
return null
}
- const fromProps = borderRadiusFromProps(jsxElement.props)
+ const fromStyleInfo = borderRadiusFromStyleInfo(styleInfo)
const measurementsNonZero = AllSides.some((c) => {
const measurement = renderedValueSides[c]
if (measurement == null) {
@@ -250,7 +246,7 @@ export function borderRadiusFromElement(
if (
!(
elementIsIntrinsicElementOrScene ||
- shouldShowControls(fromProps != null, measurementsNonZero)
+ shouldShowControls(fromStyleInfo != null, measurementsNonZero)
)
) {
return null
@@ -258,7 +254,7 @@ export function borderRadiusFromElement(
const borderRadius = optionalMap(
(radius) => measurementFromBorderRadius(renderedValueSides, radius),
- fromProps,
+ fromStyleInfo,
)
const defaultBorderRadiusSides = borderRadiusSidesFromValue(
@@ -281,7 +277,7 @@ export function borderRadiusFromElement(
const borderRadiusMinMax = { min: 0, max: borderRadiusUpperLimit }
return {
- mode: fromProps?.type === 'sides' ? 'individual' : 'all',
+ mode: fromStyleInfo?.type === 'sides' ? 'individual' : 'all',
borderRadius: mapBorderRadiusSides(
(n) => adjustBorderRadius(borderRadiusMinMax, n),
borderRadius ?? defaultBorderRadiusSides,
@@ -300,36 +296,20 @@ interface BorderRadiusFromProps {
sides: BorderRadiusSides
}
-function borderRadiusFromProps(props: JSXAttributes): BorderRadiusFromProps | null {
- const wrappedProps = right(props)
+function borderRadiusFromStyleInfo(styleInfo: StyleInfo | null): BorderRadiusFromProps | null {
+ const borderRadius = optionalMap(maybePropertyValue, styleInfo?.borderRadius)
- const borderRadius = getLayoutProperty('borderRadius', wrappedProps, styleStringInArray)
- const simpleBorderRadius = foldEither(
- () => null,
- (radius) => {
- if (radius == null) {
- return null
- } else {
- return foldEither(borderRadiusSidesFromValue, (value) => value, radius)
- }
- },
+ const simpleBorderRadius = optionalMap(
+ (radius) => foldEither(borderRadiusSidesFromValue, (value) => value, radius),
borderRadius,
)
- const borderTopLeftRadius = defaultEither(
- null,
- getLayoutProperty('borderTopLeftRadius', wrappedProps, styleStringInArray),
- )
- const borderTopRightRadius = defaultEither(
- null,
- getLayoutProperty('borderTopRightRadius', wrappedProps, styleStringInArray),
- )
- const borderBottomLeftRadius = defaultEither(
- null,
- getLayoutProperty('borderBottomLeftRadius', wrappedProps, styleStringInArray),
- )
- const borderBottomRightRadius = defaultEither(
- null,
- getLayoutProperty('borderBottomRightRadius', wrappedProps, styleStringInArray),
+
+ const borderTopLeftRadius = optionalMap(maybePropertyValue, styleInfo?.borderTopLeftRadius)
+ const borderTopRightRadius = optionalMap(maybePropertyValue, styleInfo?.borderTopRightRadius)
+ const borderBottomLeftRadius = optionalMap(maybePropertyValue, styleInfo?.borderBottomLeftRadius)
+ const borderBottomRightRadius = optionalMap(
+ maybePropertyValue,
+ styleInfo?.borderBottomRightRadius,
)
if (
@@ -606,27 +586,15 @@ const setShorthandStylePropertyCommand =
function getAddOverflowHiddenCommands(
target: ElementPath,
- projectContents: ProjectContentTreeRoot,
+ styleInfo: StyleInfo | null,
): Array {
- const overflowProp = PP.create('style', 'overflow')
-
- const propertyExists = withUnderlyingTarget(target, projectContents, false, (_, element) => {
- if (isJSXElement(element)) {
- return foldEither(
- () => false,
- (value) => !modifiableAttributeIsAttributeNotFound(value),
- getModifiableJSXAttributeAtPath(element.props, overflowProp),
- )
- } else {
- return false
- }
- })
-
+ const propertyExists = styleInfo?.overflow != null && styleInfo.overflow.type !== 'not-found'
if (propertyExists) {
return []
}
+
return [
showToastCommand('Element now hides overflowing content', 'NOTICE', 'property-added'),
- setProperty('always', target, overflowProp, 'hidden'),
+ setProperty('always', target, StyleProp('overflow'), 'hidden'),
]
}
diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts
index 218f168e7dec..d0e222a88ac3 100644
--- a/editor/src/components/canvas/canvas-types.ts
+++ b/editor/src/components/canvas/canvas-types.ts
@@ -27,7 +27,14 @@ import type {
import { InteractionSession } from './canvas-strategies/interaction-state'
import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types'
import type { MouseButtonsPressed } from '../../utils/mouse'
-import type { CSSNumber, CSSPadding, FlexDirection } from '../inspector/common/css-utils'
+import type {
+ CSSBorderRadius,
+ CSSBorderRadiusIndividual,
+ CSSNumber,
+ CSSOverflow,
+ CSSPadding,
+ FlexDirection,
+} from '../inspector/common/css-utils'
export const CanvasContainerID = 'canvas-container'
@@ -594,7 +601,10 @@ export type HeightInfo = CSSStyleProperty
export type FlexBasisInfo = CSSStyleProperty
export type PaddingInfo = CSSStyleProperty
export type PaddingSideInfo = CSSStyleProperty
+export type BorderRadiusInfo = CSSStyleProperty
+export type BorderRadiusCornerInfo = CSSStyleProperty
export type ZIndexInfo = CSSStyleProperty
+export type OverflowInfo = CSSStyleProperty
export interface StyleInfo {
gap: FlexGapInfo | null
@@ -611,7 +621,13 @@ export interface StyleInfo {
paddingRight: PaddingSideInfo | null
paddingBottom: PaddingSideInfo | null
paddingLeft: PaddingSideInfo | null
+ borderRadius: BorderRadiusInfo | null
+ borderTopLeftRadius: BorderRadiusCornerInfo | null
+ borderTopRightRadius: BorderRadiusCornerInfo | null
+ borderBottomRightRadius: BorderRadiusCornerInfo | null
+ borderBottomLeftRadius: BorderRadiusCornerInfo | null
zIndex: ZIndexInfo | null
+ overflow: OverflowInfo | null
}
const emptyStyleInfo: StyleInfo = {
@@ -629,7 +645,13 @@ const emptyStyleInfo: StyleInfo = {
paddingRight: null,
paddingBottom: null,
paddingLeft: null,
+ borderRadius: null,
+ borderTopLeftRadius: null,
+ borderTopRightRadius: null,
+ borderBottomRightRadius: null,
+ borderBottomLeftRadius: null,
zIndex: null,
+ overflow: null,
}
export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo
diff --git a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx
index f1b733c9811e..eb0d98c4d493 100644
--- a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx
+++ b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx
@@ -1,6 +1,5 @@
import createCachedSelector from 're-reselect'
import React from 'react'
-import { createSelector } from 'reselect'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
import type { CanvasVector, Size } from '../../../../core/shared/math-utils'
@@ -15,6 +14,7 @@ import { Substores, useEditorState, useRefEditorState } from '../../../editor/st
import type {
CanvasSubstate,
MetadataSubstate,
+ StyleInfoSubstate,
} from '../../../editor/store/store-hook-substore-types'
import { printCSSNumber } from '../../../inspector/common/css-utils'
import { metadataSelector } from '../../../inspector/inpector-selectors'
@@ -24,7 +24,6 @@ import {
BorderRadiusCorners,
BorderRadiusHandleHitArea,
BorderRadiusHandleSize,
- BorderRadiusSides,
handlePosition,
} from '../../border-radius-control-utils'
import CanvasActions from '../../canvas-actions'
@@ -41,6 +40,8 @@ import { CanvasOffsetWrapper } from '../canvas-offset-wrapper'
import { isZeroSizedElement } from '../outline-utils'
import type { CSSNumberWithRenderedValue } from './controls-common'
import { CanvasLabel, fallbackEmptyValue } from './controls-common'
+import { getActivePlugin } from '../../plugins/style-plugins'
+import type { EditorStorePatched } from '../../../editor/store/editor-state'
export const CircularHandleTestId = (corner: BorderRadiusCorner): string =>
`circular-handle-${corner}`
@@ -59,13 +60,17 @@ const isDraggingSelector = (store: CanvasSubstate): boolean => {
const borderRadiusSelector = createCachedSelector(
metadataSelector,
+ (store: StyleInfoSubstate) =>
+ getActivePlugin(store.editor).styleInfoFactory({
+ projectContents: store.editor.projectContents,
+ }),
(_: MetadataSubstate, x: ElementPath) => x,
- (metadata, selectedElement) => {
+ (metadata, styleInfoReader, selectedElement) => {
const element = MetadataUtils.findElementByElementPath(metadata, selectedElement)
if (element == null) {
return null
}
- return borderRadiusFromElement(element)
+ return borderRadiusFromElement(element, styleInfoReader(selectedElement))
},
)((_, x) => EP.toString(x))
@@ -117,7 +122,7 @@ export const BorderRadiusControl = controlForStrategyMemoized borderRadiusSelector(store, selectedElement),
'BorderRadiusControl borderRadius',
)
diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts
index 226ff8a45401..a3d0c7c3376b 100644
--- a/editor/src/components/canvas/plugins/inline-style-plugin.ts
+++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts
@@ -78,8 +78,21 @@ export const InlineStylePlugin: StylePlugin = {
const paddingBottom = getPropertyFromInstance('paddingBottom', element.props)
const paddingLeft = getPropertyFromInstance('paddingLeft', element.props)
const paddingRight = getPropertyFromInstance('paddingRight', element.props)
+ const borderRadius = getPropertyFromInstance('borderRadius', element.props)
+ const borderTopLeftRadius = getPropertyFromInstance('borderTopLeftRadius', element.props)
+ const borderTopRightRadius = getPropertyFromInstance('borderTopRightRadius', element.props)
+ const borderBottomRightRadius = getPropertyFromInstance(
+ 'borderBottomRightRadius',
+ element.props,
+ )
+ const borderBottomLeftRadius = getPropertyFromInstance(
+ 'borderBottomLeftRadius',
+ element.props,
+ )
const zIndex = getPropertyFromInstance('zIndex', element.props)
+ const overflow = getPropertyFromInstance('overflow', element.props)
+
return {
gap: gap,
flexDirection: flexDirection,
@@ -95,7 +108,13 @@ export const InlineStylePlugin: StylePlugin = {
paddingBottom: paddingBottom,
paddingLeft: paddingLeft,
paddingRight: paddingRight,
+ borderRadius: borderRadius,
+ borderTopLeftRadius: borderTopLeftRadius,
+ borderTopRightRadius: borderTopRightRadius,
+ borderBottomRightRadius: borderBottomRightRadius,
+ borderBottomLeftRadius: borderBottomLeftRadius,
zIndex: zIndex,
+ overflow: overflow,
}
},
updateStyles: (editorState, elementPath, updates) => {
diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts
index c23215963f48..42ecae83b215 100644
--- a/editor/src/components/canvas/plugins/style-plugins.ts
+++ b/editor/src/components/canvas/plugins/style-plugins.ts
@@ -20,6 +20,7 @@ import * as PP from '../../../core/shared/property-path'
import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template'
import type { CSSStyleProperty } from '../canvas-types'
import { isStyleInfoKey, type StyleInfo } from '../canvas-types'
+import type { StyleInfoSubEditorState } from '../../editor/store/store-hook-substore-types'
import type { ParsedCSSProperties } from '../../inspector/common/css-utils'
export interface UpdateCSSProp {
@@ -64,7 +65,7 @@ export interface StylePlugin {
) => EditorStateWithPatch
}
-export function getActivePlugin(editorState: EditorState): StylePlugin {
+export function getActivePlugin(editorState: StyleInfoSubEditorState): StylePlugin {
if (isFeatureEnabled('Tailwind') && isTailwindEnabled()) {
return TailwindPlugin(getTailwindConfigCached(editorState))
}
@@ -177,6 +178,13 @@ const genericPropPatcher =
const PaddingLonghands = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight']
+const BorderRadiusLonghands = [
+ 'borderTopLeftRadius',
+ 'borderTopRightRadius',
+ 'borderBottomLeftRadius',
+ 'borderBottomRightRadius',
+]
+
const patchers: PropPatcher[] = [
{ matches: (p) => p === 'gap', patch: genericPropPatcher('0px') },
{
@@ -193,7 +201,25 @@ const patchers: PropPatcher[] = [
)
},
},
+ {
+ matches: (p) => p === 'borderRadius',
+ patch: (_, styleInfo, updatedProperties) => {
+ const propIsSetOnElement = styleInfo?.padding != null
+
+ if (!propIsSetOnElement) {
+ return []
+ }
+
+ return BorderRadiusLonghands.filter(
+ (p) => !updatedProperties.propertiesUpdated.includes(p),
+ ).map((p) => makeZeroProp(p))
+ },
+ },
{ matches: (p) => PaddingLonghands.includes(p), patch: genericPropPatcher('0px') },
+ {
+ matches: (p) => BorderRadiusLonghands.includes(p),
+ patch: genericPropPatcher('0px'),
+ },
]
function getPropertiesToZero(
diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts
index 768f66dd6207..dd9659424cfa 100644
--- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts
+++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts
@@ -2,13 +2,13 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser'
import { defaultEither, flatMapEither, isLeft } from '../../../core/shared/either'
import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options'
import { getElementFromProjectContents } from '../../editor/store/editor-state'
-import type { ParsedCSSProperties, Parser } from '../../inspector/common/css-utils'
+import type { ParsedCSSProperties } from '../../inspector/common/css-utils'
import { cssParsers } from '../../inspector/common/css-utils'
import { mapDropNulls } from '../../../core/shared/array-utils'
import type { StylePlugin } from './style-plugins'
import type { Config } from 'tailwindcss/types/config'
import type { StyleInfo } from '../canvas-types'
-import { cssStyleProperty, isStyleInfoKey, type CSSStyleProperty } from '../canvas-types'
+import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types'
import * as UCL from './tailwind-style-plugin-utils/update-class-list'
import { assertNever } from '../../../core/shared/utils'
import {
@@ -45,6 +45,12 @@ const TailwindPropertyMapping: Record = {
paddingBottom: 'paddingBottom',
paddingLeft: 'paddingLeft',
+ borderRadius: 'borderRadius',
+ borderTopLeftRadius: 'borderTopLeftRadius',
+ borderTopRightRadius: 'borderTopRightRadius',
+ borderBottomRightRadius: 'borderBottomRightRadius',
+ borderBottomLeftRadius: 'borderBottomLeftRadius',
+
justifyContent: 'justifyContent',
alignItems: 'alignItems',
flex: 'flex',
@@ -55,6 +61,8 @@ const TailwindPropertyMapping: Record = {
flexWrap: 'flexWrap',
gap: 'gap',
+ overflow: 'overflow',
+
zIndex: 'zIndex',
}
@@ -154,7 +162,28 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({
mapping[TailwindPropertyMapping.paddingLeft],
'paddingLeft',
),
+ borderRadius: parseTailwindProperty(
+ mapping[TailwindPropertyMapping.borderRadius],
+ 'borderRadius',
+ ),
+ borderTopLeftRadius: parseTailwindProperty(
+ mapping[TailwindPropertyMapping.borderTopLeftRadius],
+ 'borderTopLeftRadius',
+ ),
+ borderTopRightRadius: parseTailwindProperty(
+ mapping[TailwindPropertyMapping.borderTopRightRadius],
+ 'borderTopRightRadius',
+ ),
+ borderBottomRightRadius: parseTailwindProperty(
+ mapping[TailwindPropertyMapping.borderBottomRightRadius],
+ 'borderBottomRightRadius',
+ ),
+ borderBottomLeftRadius: parseTailwindProperty(
+ mapping[TailwindPropertyMapping.borderBottomLeftRadius],
+ 'borderBottomLeftRadius',
+ ),
zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'),
+ overflow: parseTailwindProperty(mapping[TailwindPropertyMapping.overflow], 'overflow'),
}
},
updateStyles: (editorState, elementPath, updates) => {
diff --git a/editor/src/components/editor/store/store-hook-substore-types.ts b/editor/src/components/editor/store/store-hook-substore-types.ts
index 178ab22a95d2..eea6a848a3f9 100644
--- a/editor/src/components/editor/store/store-hook-substore-types.ts
+++ b/editor/src/components/editor/store/store-hook-substore-types.ts
@@ -158,6 +158,18 @@ const propertyControlsInfoSubstate = {
} as const
export type PropertyControlsInfoSubstate = typeof propertyControlsInfoSubstate
+// StyleInfoSubstate
+export const styleInfoSubstateKeys = [
+ ...metadataSubstateKeys,
+ ...projectContentsKeys,
+ 'codeResultCache',
+] as const
+const emptyStyleInfoSubstate = {
+ editor: pick(styleInfoSubstateKeys, EmptyEditorStateForKeysOnly),
+} as const
+export type StyleInfoSubstate = typeof emptyStyleInfoSubstate
+export type StyleInfoSubEditorState = StyleInfoSubstate['editor']
+
export type MetadataAndPropertyControlsInfoSubstate = MetadataSubstate &
PropertyControlsInfoSubstate
diff --git a/editor/src/components/editor/store/store-hook.ts b/editor/src/components/editor/store/store-hook.ts
index 9ff6287b8a82..ea0824397cfc 100644
--- a/editor/src/components/editor/store/store-hook.ts
+++ b/editor/src/components/editor/store/store-hook.ts
@@ -40,6 +40,7 @@ import type {
RestOfEditorState,
SelectedViewsSubstate,
StoreKey,
+ StyleInfoSubstate,
Substates,
ThemeSubstate,
UserStateSubstate,
@@ -58,6 +59,7 @@ import {
restOfEditorStateKeys,
restOfStoreKeys,
selectedViewsSubstateKeys,
+ styleInfoSubstateKeys,
variablesInScopeSubstateKeys,
} from './store-hook-substore-types'
import { Getter } from '../hook-utils'
@@ -355,6 +357,9 @@ export const Substores = {
b.editor,
)
},
+ styleInfo: (a: StyleInfoSubstate, b: StyleInfoSubstate) => {
+ return keysEquality(styleInfoSubstateKeys, a.editor, b.editor)
+ },
} as const
export const SubstateEqualityFns: {
diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts
index 4a381243fdc5..6147a5ce6441 100644
--- a/editor/src/components/inspector/common/css-utils.ts
+++ b/editor/src/components/inspector/common/css-utils.ts
@@ -2483,7 +2483,7 @@ function printTransformOrigin(transformOrigin: CSSTransformOrigin): JSExpression
)
}
-type CSSOverflow = boolean
+export type CSSOverflow = boolean
function parseOverflow(overflow: unknown): Either {
if (typeof overflow === 'string') {
diff --git a/editor/src/core/layout/layout-helpers-new.ts b/editor/src/core/layout/layout-helpers-new.ts
index c3329258a2e6..a43d7eee3587 100644
--- a/editor/src/core/layout/layout-helpers-new.ts
+++ b/editor/src/core/layout/layout-helpers-new.ts
@@ -109,6 +109,7 @@ export type StyleLayoutProp =
| 'zIndex'
| 'rowGap'
| 'columnGap'
+ | 'overflow'
export function framePointForPinnedProp(pinnedProp: LayoutPinnedProp): FramePoint {
switch (pinnedProp) {
diff --git a/editor/src/core/tailwind/tailwind-compilation.ts b/editor/src/core/tailwind/tailwind-compilation.ts
index b71edcc1b587..8a61463711ab 100644
--- a/editor/src/core/tailwind/tailwind-compilation.ts
+++ b/editor/src/core/tailwind/tailwind-compilation.ts
@@ -13,15 +13,16 @@ import { importDefault } from '../es-modules/commonjs-interop'
import { rescopeCSSToTargetCanvasOnly } from '../shared/css-utils'
import type { RequireFn } from '../shared/npm-dependency-types'
import { TailwindConfigPath } from './tailwind-config'
-import { ElementsToRerenderGLOBAL } from '../../components/canvas/ui-jsx-canvas'
import { isFeatureEnabled } from '../../utils/feature-switches'
import type { Config } from 'tailwindcss/types/config'
-import type { EditorState } from '../../components/editor/store/editor-state'
import { createSelector } from 'reselect'
-import type { ProjectContentSubstate } from '../../components/editor/store/store-hook-substore-types'
+import type {
+ ProjectContentSubstate,
+ StyleInfoSubEditorState,
+} from '../../components/editor/store/store-hook-substore-types'
const LatestConfig: { current: { code: string; config: Config } | null } = { current: null }
-export function getTailwindConfigCached(editorState: EditorState): Config | null {
+export function getTailwindConfigCached(editorState: StyleInfoSubEditorState): Config | null {
const tailwindConfig = getProjectFileByFilePath(editorState.projectContents, TailwindConfigPath)
if (tailwindConfig == null || tailwindConfig.type !== 'TEXT_FILE') {
return null
@@ -59,6 +60,8 @@ function ensureElementExists({ type, id }: { type: string; id: string }) {
return tag
}
+const TailwindStylesElementID = 'utopia-tailwind-jit-styles'
+
async function generateTailwindStyles(tailwindCss: Tailwindcss, allCSSFiles: string) {
const contentElement = document.getElementById(CanvasContainerID)
if (contentElement == null) {
@@ -66,10 +69,17 @@ async function generateTailwindStyles(tailwindCss: Tailwindcss, allCSSFiles: str
}
const content = contentElement.outerHTML
const styleString = await tailwindCss.generateStylesFromContent(allCSSFiles, [content])
- const style = ensureElementExists({ type: 'style', id: 'utopia-tailwind-jit-styles' })
+ const style = ensureElementExists({ type: 'style', id: TailwindStylesElementID })
style.textContent = rescopeCSSToTargetCanvasOnly(styleString)
}
+function removeTailwindStyles() {
+ const style = document.getElementById(TailwindStylesElementID)
+ if (style != null) {
+ style.remove()
+ }
+}
+
function getCssFilesFromProjectContents(projectContents: ProjectContentTreeRoot) {
let files: string[] = []
walkContentsTree(projectContents, (path, file) => {
@@ -99,11 +109,7 @@ function runTailwindClassGenerationOnDOMMutation(
m.addedNodes.length > 0 || // new DOM element was added with potentially new classes
m.attributeName === 'class', // potentially new classes were added to the class attribute of an element
)
- if (
- !updateHasNewTailwindData ||
- isInteractionActive ||
- ElementsToRerenderGLOBAL.current !== 'rerender-all-elements' // implies that an interaction is in progress)
- ) {
+ if (!updateHasNewTailwindData || isInteractionActive) {
return
}
generateTailwindClasses(projectContents, requireFn)
@@ -161,6 +167,7 @@ export const useTailwindCompilation = () => {
generateTailwindClasses(projectContentsRef.current, requireFnRef.current)
return () => {
+ removeTailwindStyles()
observer.disconnect()
}
}, [isInteractionActiveRef, projectContentsRef, requireFnRef, tailwindConfig])