From ca15b4f2f5553561ac89ab0f3116699bf2c98097 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Wed, 20 Nov 2024 17:27:16 +0100 Subject: [PATCH 01/21] wip - conversion --- editor/src/components/canvas/canvas-types.ts | 60 +++++- .../components/canvas/commands/commands.ts | 5 + ...nline-style-tailwind-conversion-command.ts | 174 ++++++++++++++++++ .../canvas/plugins/style-plugins.ts | 2 +- editor/src/components/context-menu-items.ts | 25 +++ .../src/components/element-context-menu.tsx | 4 + 6 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 010fb804613f..f4b7c78a57ce 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -26,7 +26,13 @@ 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 { + printCSSNumber, + type CSSNumber, + type CSSPadding, + type FlexDirection, +} from '../inspector/common/css-utils' +import { optionalMap } from '../../core/shared/optional-utils' export const CanvasContainerID = 'canvas-container' @@ -625,3 +631,55 @@ const emptyStyleInfo: StyleInfo = { } export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo + +function mapCSSStyleProperty( + property: CSSStyleProperty | null, + map: (value: T) => U, +): U | null { + if (property === null || property.type !== 'property') { + return null + } + return map(property.value) +} + +export function stringifyStyleInfo( + styleInfo: StyleInfo, +): Record { + return { + gap: mapCSSStyleProperty(styleInfo.gap, (gap) => printCSSNumber(gap, null)), + flexDirection: mapCSSStyleProperty(styleInfo.flexDirection, (flexDirection) => flexDirection), + left: mapCSSStyleProperty(styleInfo.left, (left) => printCSSNumber(left, null)), + right: mapCSSStyleProperty(styleInfo.right, (right) => printCSSNumber(right, null)), + top: mapCSSStyleProperty(styleInfo.top, (top) => printCSSNumber(top, null)), + bottom: mapCSSStyleProperty(styleInfo.bottom, (bottom) => printCSSNumber(bottom, null)), + width: mapCSSStyleProperty(styleInfo.width, (width) => printCSSNumber(width, null)), + height: mapCSSStyleProperty(styleInfo.height, (height) => printCSSNumber(height, null)), + flexBasis: mapCSSStyleProperty(styleInfo.flexBasis, (flexBasis) => + printCSSNumber(flexBasis, null), + ), + padding: mapCSSStyleProperty( + styleInfo.padding, + (padding) => + `${printCSSNumber(padding.paddingTop, null)} ${printCSSNumber( + padding.paddingRight, + null, + )} ${printCSSNumber(padding.paddingBottom, null)} ${printCSSNumber( + padding.paddingLeft, + null, + )}`, + ), + paddingTop: mapCSSStyleProperty(styleInfo.paddingTop, (paddingTop) => + printCSSNumber(paddingTop, null), + ), + paddingRight: mapCSSStyleProperty(styleInfo.paddingRight, (paddingRight) => + printCSSNumber(paddingRight, null), + ), + paddingBottom: mapCSSStyleProperty(styleInfo.paddingBottom, (paddingBottom) => + printCSSNumber(paddingBottom, null), + ), + paddingLeft: mapCSSStyleProperty(styleInfo.paddingLeft, (paddingLeft) => + printCSSNumber(paddingLeft, null), + ), + zIndex: mapCSSStyleProperty(styleInfo.zIndex, (zIndex) => printCSSNumber(zIndex, null)), + } +} diff --git a/editor/src/components/canvas/commands/commands.ts b/editor/src/components/canvas/commands/commands.ts index 7ce5e9b1e6ae..de839ea3b0d2 100644 --- a/editor/src/components/canvas/commands/commands.ts +++ b/editor/src/components/canvas/commands/commands.ts @@ -78,6 +78,8 @@ import { runShowGridControlsCommand, type ShowGridControlsCommand, } from './show-grid-controls-command' +import type { InlineStyleTailwindConversionCommand } from './inline-style-tailwind-conversion-command' +import { runInlineStyleTailwindConversionCommand } from './inline-style-tailwind-conversion-command' export interface CommandFunctionResult { editorStatePatches: Array @@ -129,6 +131,7 @@ export type CanvasCommand = | SetActiveFrames | UpdateBulkProperties | ShowGridControlsCommand + | InlineStyleTailwindConversionCommand export function runCanvasCommand( editorState: EditorState, @@ -208,6 +211,8 @@ export function runCanvasCommand( return runSetActiveFrames(editorState, command) case 'SHOW_GRID_CONTROLS': return runShowGridControlsCommand(editorState, command) + case 'INLINE_STYLE_TAILWIND_CONVERSION': + return runInlineStyleTailwindConversionCommand(editorState, command) default: const _exhaustiveCheck: never = command throw new Error(`Unhandled canvas command ${JSON.stringify(command)}`) diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts new file mode 100644 index 000000000000..74329c5ba28e --- /dev/null +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -0,0 +1,174 @@ +import type { ElementPath } from 'utopia-shared/src/types' +import type { BaseCommand, CommandFunctionResult } from './commands' +import type { EditorState, EditorStatePatch } from '../../editor/store/editor-state' +import type { EditorStateWithPatches, StyleUpdate } from '../plugins/style-plugins' +import { InlineStylePlugin } from '../plugins/inline-style-plugin' +import { stringifyStyleInfo } from '../canvas-types' +import { TailwindPlugin } from '../plugins/tailwind-style-plugin' +import { getTailwindConfigCached } from '../../../core/tailwind/tailwind-compilation' +import { mapDropNulls } from '../../../core/shared/array-utils' +import { assertNever } from '../../../core/shared/utils' + +export interface InlineStyleTailwindConversionCommand extends BaseCommand { + type: 'INLINE_STYLE_TAILWIND_CONVERSION' + + direction: 'TO_INLINE_STYLE' | 'TO_TAILWIND' + elementPaths: ElementPath[] +} + +export function inlineStyleTailwindConversionCommand( + whenToRun: 'always' | 'on-complete', + direction: 'TO_INLINE_STYLE' | 'TO_TAILWIND', + elementPaths: ElementPath[], +): InlineStyleTailwindConversionCommand { + return { + type: 'INLINE_STYLE_TAILWIND_CONVERSION', + whenToRun: whenToRun, + direction: direction, + elementPaths: elementPaths, + } +} + +/** + * wholesale conversion + * + * from inline style to tailwind: + * - convert inline style to css: https://www.npmjs.com/package/style-object-to-css-string + * - convert css to tailwind https://github.com/hymhub/css-to-tailwind + * + * from tailwind to inline style: + * - convert tailwind to css: + * - convert css to react: https://github.com/transform-it/transform-css-to-js + */ + +function convertInlineStyleToTailwind( + editorState: EditorState, + elementPaths: ElementPath[], +): EditorStateWithPatches { + let patches: EditorStatePatch[] = [] + let editorStateWithChanges: EditorState = editorState + elementPaths.forEach((elementPath) => { + const styleInfo = InlineStylePlugin.styleInfoFactory({ + projectContents: editorState.projectContents, + })(elementPath) + if (styleInfo == null) { + return + } + const styleInfoString = stringifyStyleInfo(styleInfo) + const stylesToAdd: StyleUpdate[] = mapDropNulls( + ([property, value]) => + value == null + ? null + : { + property: property, + value: value, + type: 'set', + }, + Object.entries(styleInfoString), + ) + + const stylesToRemove: StyleUpdate[] = stylesToAdd.map((style) => ({ + property: style.property, + type: 'delete', + })) + + const { editorStateWithChanges: updatedEditorState } = TailwindPlugin( + getTailwindConfigCached(editorStateWithChanges), + ).updateStyles(editorStateWithChanges, elementPath, stylesToAdd) + const { editorStatePatch: editorStatePatchToRemove, editorStateWithChanges: finalEditorState } = + InlineStylePlugin.updateStyles(updatedEditorState, elementPath, stylesToRemove) + patches.push(editorStatePatchToRemove) + editorStateWithChanges = finalEditorState + }) + + return { + editorStateWithChanges: editorStateWithChanges, + editorStatePatches: patches, + } +} + +function convertTailwindToInlineStyle( + editorState: EditorState, + elementPaths: ElementPath[], +): EditorStateWithPatches { + let patches: EditorStatePatch[] = [] + let editorStateWithChanges: EditorState = editorState + + elementPaths.forEach((elementPath) => { + const styleInfo = TailwindPlugin( + getTailwindConfigCached(editorStateWithChanges), + ).styleInfoFactory({ + projectContents: editorStateWithChanges.projectContents, + })(elementPath) + + if (styleInfo == null) { + return + } + const styleInfoString = stringifyStyleInfo(styleInfo) + const stylesToAdd: StyleUpdate[] = mapDropNulls( + ([property, value]) => + value == null + ? null + : { + property: property, + value: value, + type: 'set', + }, + Object.entries(styleInfoString), + ) + + const stylesToRemove: StyleUpdate[] = stylesToAdd.map((style) => ({ + property: style.property, + type: 'delete', + })) + + const { editorStateWithChanges: updatedEditorState } = InlineStylePlugin.updateStyles( + editorStateWithChanges, + elementPath, + stylesToAdd, + ) + const { editorStatePatch: editorStatePatchToRemove, editorStateWithChanges: finalEditorState } = + TailwindPlugin(getTailwindConfigCached(updatedEditorState)).updateStyles( + updatedEditorState, + elementPath, + stylesToRemove, + ) + patches.push(editorStatePatchToRemove) + editorStateWithChanges = finalEditorState + }) + + return { + editorStateWithChanges: editorState, + editorStatePatches: [], + } +} + +function runConversionWithStylePlugins( + editorState: EditorState, + elementPaths: ElementPath[], + direction: 'TO_INLINE_STYLE' | 'TO_TAILWIND', +): EditorStateWithPatches { + switch (direction) { + case 'TO_INLINE_STYLE': + return convertTailwindToInlineStyle(editorState, elementPaths) + case 'TO_TAILWIND': + return convertInlineStyleToTailwind(editorState, elementPaths) + default: + assertNever(direction) + } +} + +export function runInlineStyleTailwindConversionCommand( + editorState: EditorState, + command: InlineStyleTailwindConversionCommand, +): CommandFunctionResult { + const { editorStatePatches } = runConversionWithStylePlugins( + editorState, + command.elementPaths, + command.direction, + ) + return { + commandDescription: 'Inline Style Tailwind Conversion', + editorStatePatches: editorStatePatches, + } +} diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index 5fa2e0aed628..6392a62529ea 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -98,7 +98,7 @@ function ensureElementPathInUpdatedPropertiesGlobal( return updatedPropertiesToExtend } -interface EditorStateWithPatches { +export interface EditorStateWithPatches { editorStateWithChanges: EditorState editorStatePatches: EditorStatePatch[] } diff --git a/editor/src/components/context-menu-items.ts b/editor/src/components/context-menu-items.ts index be80eacad58f..6cda79d418d5 100644 --- a/editor/src/components/context-menu-items.ts +++ b/editor/src/components/context-menu-items.ts @@ -54,6 +54,7 @@ import { type ShowComponentPickerContextMenuCallback, renderPropTarget, } from './navigator/navigator-item/component-picker-context-menu' +import { inlineStyleTailwindConversionCommand } from './canvas/commands/inline-style-tailwind-conversion-command' export interface ContextMenuItem { name: string | React.ReactNode @@ -563,3 +564,27 @@ export const escapeHatch: ContextMenuItem = { } }, } + +export const convertInlineStyleToTailwindStyle: ContextMenuItem = { + name: 'Convert Inline Style to Tailwind Style', + enabled: true, + action: (data, dispatch?: EditorDispatch) => { + dispatch?.([ + EditorActions.applyCommandsAction([ + inlineStyleTailwindConversionCommand('always', 'TO_TAILWIND', data.selectedViews), + ]), + ]) + }, +} + +export const convertTailwindStyleToInlineStyle: ContextMenuItem = { + name: 'Convert Tailwind Style to Inline Style', + enabled: true, + action: (data, dispatch?: EditorDispatch) => { + dispatch?.([ + EditorActions.applyCommandsAction([ + inlineStyleTailwindConversionCommand('always', 'TO_INLINE_STYLE', data.selectedViews), + ]), + ]) + }, +} diff --git a/editor/src/components/element-context-menu.tsx b/editor/src/components/element-context-menu.tsx index 91f0f5d85587..1b26cc2ef31e 100644 --- a/editor/src/components/element-context-menu.tsx +++ b/editor/src/components/element-context-menu.tsx @@ -31,6 +31,8 @@ import { pasteHere, replace, toggleCanCondense, + convertInlineStyleToTailwindStyle, + convertTailwindStyleToInlineStyle, } from './context-menu-items' import { ContextMenu } from './context-menu-wrapper' import { useRefEditorState, useEditorState, Substores } from './editor/store/store-hook' @@ -69,6 +71,8 @@ interface ElementContextMenuProps { } const ElementContextMenuItems: Array> = [ + convertInlineStyleToTailwindStyle, + convertTailwindStyleToInlineStyle, setAsFocusedElement, removeAsFocusedElement, scrollToElement, From 09d26000bf7b4d0d96005ababfac51fefbe7c8e0 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 21 Nov 2024 10:03:24 +0100 Subject: [PATCH 02/21] factor out common parts for cursor --- ...nline-style-tailwind-conversion-command.ts | 67 +++++++-------- .../canvas/plugins/style-plugins.ts | 2 +- .../canvas/plugins/tailwind-style-plugin.ts | 86 ++++++++----------- 3 files changed, 70 insertions(+), 85 deletions(-) diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts index 74329c5ba28e..c83606f2f2be 100644 --- a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -1,8 +1,9 @@ import type { ElementPath } from 'utopia-shared/src/types' import type { BaseCommand, CommandFunctionResult } from './commands' import type { EditorState, EditorStatePatch } from '../../editor/store/editor-state' -import type { EditorStateWithPatches, StyleUpdate } from '../plugins/style-plugins' +import type { DeleteCSSProp, EditorStateWithPatches, UpdateCSSProp } from '../plugins/style-plugins' import { InlineStylePlugin } from '../plugins/inline-style-plugin' +import type { StyleInfo } from '../canvas-types' import { stringifyStyleInfo } from '../canvas-types' import { TailwindPlugin } from '../plugins/tailwind-style-plugin' import { getTailwindConfigCached } from '../../../core/tailwind/tailwind-compilation' @@ -41,6 +42,31 @@ export function inlineStyleTailwindConversionCommand( * - convert css to react: https://github.com/transform-it/transform-css-to-js */ +function getStyleInfoUpdates(styleInfo: StyleInfo): { + stylesToAdd: UpdateCSSProp[] + stylesToRemove: DeleteCSSProp[] +} { + const styleInfoString = stringifyStyleInfo(styleInfo) + const stylesToAdd: UpdateCSSProp[] = mapDropNulls( + ([property, value]) => + value == null + ? null + : { + property: property, + value: value, + type: 'set', + }, + Object.entries(styleInfoString), + ) + + const stylesToRemove: DeleteCSSProp[] = stylesToAdd.map((style) => ({ + property: style.property, + type: 'delete', + })) + + return { stylesToAdd, stylesToRemove } +} + function convertInlineStyleToTailwind( editorState: EditorState, elementPaths: ElementPath[], @@ -51,26 +77,12 @@ function convertInlineStyleToTailwind( const styleInfo = InlineStylePlugin.styleInfoFactory({ projectContents: editorState.projectContents, })(elementPath) + if (styleInfo == null) { return } - const styleInfoString = stringifyStyleInfo(styleInfo) - const stylesToAdd: StyleUpdate[] = mapDropNulls( - ([property, value]) => - value == null - ? null - : { - property: property, - value: value, - type: 'set', - }, - Object.entries(styleInfoString), - ) - const stylesToRemove: StyleUpdate[] = stylesToAdd.map((style) => ({ - property: style.property, - type: 'delete', - })) + const { stylesToAdd, stylesToRemove } = getStyleInfoUpdates(styleInfo) const { editorStateWithChanges: updatedEditorState } = TailwindPlugin( getTailwindConfigCached(editorStateWithChanges), @@ -104,23 +116,8 @@ function convertTailwindToInlineStyle( if (styleInfo == null) { return } - const styleInfoString = stringifyStyleInfo(styleInfo) - const stylesToAdd: StyleUpdate[] = mapDropNulls( - ([property, value]) => - value == null - ? null - : { - property: property, - value: value, - type: 'set', - }, - Object.entries(styleInfoString), - ) - const stylesToRemove: StyleUpdate[] = stylesToAdd.map((style) => ({ - property: style.property, - type: 'delete', - })) + const { stylesToAdd, stylesToRemove } = getStyleInfoUpdates(styleInfo) const { editorStateWithChanges: updatedEditorState } = InlineStylePlugin.updateStyles( editorStateWithChanges, @@ -138,8 +135,8 @@ function convertTailwindToInlineStyle( }) return { - editorStateWithChanges: editorState, - editorStatePatches: [], + editorStateWithChanges: editorStateWithChanges, + editorStatePatches: patches, } } diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index eec14fe1d27c..e0a6e0ee674a 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -28,7 +28,7 @@ export interface UpdateCSSProp { value: string | number } -interface DeleteCSSProp { +export interface DeleteCSSProp { type: 'delete' property: string } diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 768f66dd6207..02613b6fa8f6 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -8,21 +8,24 @@ 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 type { JSXAttributes } from 'utopia-shared/src/types' import { jsxSimpleAttributeToValue, getModifiableJSXAttributeAtPath, } from '../../../core/shared/jsx-attribute-utils' -import { emptyComments, type JSXAttributes } from 'utopia-shared/src/types' import * as PP from '../../../core/shared/property-path' -import { jsExpressionValue } from '../../../core/shared/element-template' +import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' -function parseTailwindProperty( - value: string | number | undefined, - prop: T, -): CSSStyleProperty> | null { +const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') + +function parseTailwindProperty

( + mapping: Record, + prop: P, +): CSSStyleProperty> | null { + const value = prop === 'padding' ? underscoresToSpaces(mapping[prop]) : mapping[prop] const parsed = cssParsers[prop](value, null) if (isLeft(parsed) || parsed.value == null) { return null @@ -58,8 +61,11 @@ const TailwindPropertyMapping: Record = { zIndex: 'zIndex', } -function isSupportedTailwindProperty(prop: unknown): prop is keyof typeof TailwindPropertyMapping { - return typeof prop === 'string' && prop in TailwindPropertyMapping +function toCamelCase(str: string): string { + return str + .toLowerCase() + .replace(/([-_][a-z])/g, (ltr) => ltr.toUpperCase()) + .replace(/[^a-zA-Z]/g, '') } function stringifyPropertyValue(value: string | number): string { @@ -77,16 +83,16 @@ function getTailwindClassMapping(classes: string[], config: Config | null): Reco const mapping: Record = {} classes.forEach((className) => { const parsed = TailwindClassParser.parse(className, config ?? undefined) - if (parsed.kind === 'error' || !isSupportedTailwindProperty(parsed.property)) { + if (parsed.kind === 'error') { return } - mapping[parsed.property] = parsed.value + parsed.valueDef.class.forEach((cls: string) => { + mapping[toCamelCase(cls)] = parsed.value + }) }) return mapping } -const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') - export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', readStyleFromElementProps:

( @@ -106,7 +112,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ } const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) - return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop) + return parseTailwindProperty(mapping, prop) }, styleInfoFactory: ({ projectContents }) => @@ -122,45 +128,27 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ const mapping = getTailwindClassMapping(classList.split(' '), config) return { - gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], 'gap'), - flexDirection: parseTailwindProperty( - mapping[TailwindPropertyMapping.flexDirection], - 'flexDirection', - ), - left: parseTailwindProperty(mapping[TailwindPropertyMapping.left], 'left'), - right: parseTailwindProperty(mapping[TailwindPropertyMapping.right], 'right'), - top: parseTailwindProperty(mapping[TailwindPropertyMapping.top], 'top'), - bottom: parseTailwindProperty(mapping[TailwindPropertyMapping.bottom], 'bottom'), - width: parseTailwindProperty(mapping[TailwindPropertyMapping.width], 'width'), - height: parseTailwindProperty(mapping[TailwindPropertyMapping.height], 'height'), - flexBasis: parseTailwindProperty(mapping[TailwindPropertyMapping.flexBasis], 'flexBasis'), - padding: parseTailwindProperty( - underscoresToSpaces(mapping[TailwindPropertyMapping.padding]), - 'padding', - ), - paddingTop: parseTailwindProperty( - mapping[TailwindPropertyMapping.paddingTop], - 'paddingTop', - ), - paddingRight: parseTailwindProperty( - mapping[TailwindPropertyMapping.paddingRight], - 'paddingRight', - ), - paddingBottom: parseTailwindProperty( - mapping[TailwindPropertyMapping.paddingBottom], - 'paddingBottom', - ), - paddingLeft: parseTailwindProperty( - mapping[TailwindPropertyMapping.paddingLeft], - 'paddingLeft', - ), - zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'), + gap: parseTailwindProperty(mapping, 'gap'), + flexDirection: parseTailwindProperty(mapping, 'flexDirection'), + left: parseTailwindProperty(mapping, 'left'), + right: parseTailwindProperty(mapping, 'right'), + top: parseTailwindProperty(mapping, 'top'), + bottom: parseTailwindProperty(mapping, 'bottom'), + width: parseTailwindProperty(mapping, 'width'), + height: parseTailwindProperty(mapping, 'height'), + flexBasis: parseTailwindProperty(mapping, 'flexBasis'), + padding: parseTailwindProperty(mapping, 'padding'), + paddingTop: parseTailwindProperty(mapping, 'paddingTop'), + paddingRight: parseTailwindProperty(mapping, 'paddingRight'), + paddingBottom: parseTailwindProperty(mapping, 'paddingBottom'), + paddingLeft: parseTailwindProperty(mapping, 'paddingLeft'), + zIndex: parseTailwindProperty(mapping, 'zIndex'), } }, updateStyles: (editorState, elementPath, updates) => { const propsToDelete = mapDropNulls( (update) => - update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe + update.type !== 'delete' || TailwindPropertyMapping[update.property] == null ? null : UCL.remove(TailwindPropertyMapping[update.property]), updates, @@ -168,7 +156,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ const propsToSet = mapDropNulls( (update) => - update.type !== 'set' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe + update.type !== 'set' || TailwindPropertyMapping[update.property] == null ? null : UCL.add({ property: TailwindPropertyMapping[update.property], From f694b2f97ef38a7b91465fb4d72ddb86f1c4fdad Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 21 Nov 2024 10:08:13 +0100 Subject: [PATCH 03/21] naming --- .../inline-style-tailwind-conversion-command.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts index c83606f2f2be..b988bb51aa9f 100644 --- a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -67,7 +67,7 @@ function getStyleInfoUpdates(styleInfo: StyleInfo): { return { stylesToAdd, stylesToRemove } } -function convertInlineStyleToTailwind( +function convertInlineStyleToTailwindViaStyleInfo( editorState: EditorState, elementPaths: ElementPath[], ): EditorStateWithPatches { @@ -99,7 +99,7 @@ function convertInlineStyleToTailwind( } } -function convertTailwindToInlineStyle( +function convertTailwindToInlineStyleViaStyleInfo( editorState: EditorState, elementPaths: ElementPath[], ): EditorStateWithPatches { @@ -140,16 +140,16 @@ function convertTailwindToInlineStyle( } } -function runConversionWithStylePlugins( +function runConversionWithStylePluginsViaStyleInfo( editorState: EditorState, elementPaths: ElementPath[], direction: 'TO_INLINE_STYLE' | 'TO_TAILWIND', ): EditorStateWithPatches { switch (direction) { case 'TO_INLINE_STYLE': - return convertTailwindToInlineStyle(editorState, elementPaths) + return convertTailwindToInlineStyleViaStyleInfo(editorState, elementPaths) case 'TO_TAILWIND': - return convertInlineStyleToTailwind(editorState, elementPaths) + return convertInlineStyleToTailwindViaStyleInfo(editorState, elementPaths) default: assertNever(direction) } @@ -159,7 +159,7 @@ export function runInlineStyleTailwindConversionCommand( editorState: EditorState, command: InlineStyleTailwindConversionCommand, ): CommandFunctionResult { - const { editorStatePatches } = runConversionWithStylePlugins( + const { editorStatePatches } = runConversionWithStylePluginsViaStyleInfo( editorState, command.elementPaths, command.direction, From c91419c9a724b74480c37c8fd67c7e6d1abbc419 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 21 Nov 2024 11:18:42 +0100 Subject: [PATCH 04/21] todos --- .../canvas/commands/inline-style-tailwind-conversion-command.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts index b988bb51aa9f..ca3f9b7a2ec0 100644 --- a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -40,6 +40,8 @@ export function inlineStyleTailwindConversionCommand( * from tailwind to inline style: * - convert tailwind to css: * - convert css to react: https://github.com/transform-it/transform-css-to-js + * - [ ] check out whether tailwind can help with this + * - [ ] check out whether we can glean this info from the tailwind jit lib */ function getStyleInfoUpdates(styleInfo: StyleInfo): { From 9b2e2fb018eab1ecac941e29d8964d695b9ead50 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 22 Nov 2024 14:25:29 +0100 Subject: [PATCH 05/21] correct patch generation --- ...nline-style-tailwind-conversion-command.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts index ca3f9b7a2ec0..24614c9c1941 100644 --- a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -30,20 +30,6 @@ export function inlineStyleTailwindConversionCommand( } } -/** - * wholesale conversion - * - * from inline style to tailwind: - * - convert inline style to css: https://www.npmjs.com/package/style-object-to-css-string - * - convert css to tailwind https://github.com/hymhub/css-to-tailwind - * - * from tailwind to inline style: - * - convert tailwind to css: - * - convert css to react: https://github.com/transform-it/transform-css-to-js - * - [ ] check out whether tailwind can help with this - * - [ ] check out whether we can glean this info from the tailwind jit lib - */ - function getStyleInfoUpdates(styleInfo: StyleInfo): { stylesToAdd: UpdateCSSProp[] stylesToRemove: DeleteCSSProp[] @@ -75,6 +61,7 @@ function convertInlineStyleToTailwindViaStyleInfo( ): EditorStateWithPatches { let patches: EditorStatePatch[] = [] let editorStateWithChanges: EditorState = editorState + elementPaths.forEach((elementPath) => { const styleInfo = InlineStylePlugin.styleInfoFactory({ projectContents: editorState.projectContents, @@ -89,9 +76,11 @@ function convertInlineStyleToTailwindViaStyleInfo( const { editorStateWithChanges: updatedEditorState } = TailwindPlugin( getTailwindConfigCached(editorStateWithChanges), ).updateStyles(editorStateWithChanges, elementPath, stylesToAdd) + const { editorStatePatch: editorStatePatchToRemove, editorStateWithChanges: finalEditorState } = InlineStylePlugin.updateStyles(updatedEditorState, elementPath, stylesToRemove) - patches.push(editorStatePatchToRemove) + + patches = [editorStatePatchToRemove] editorStateWithChanges = finalEditorState }) @@ -132,7 +121,7 @@ function convertTailwindToInlineStyleViaStyleInfo( elementPath, stylesToRemove, ) - patches.push(editorStatePatchToRemove) + patches = [editorStatePatchToRemove] editorStateWithChanges = finalEditorState }) From 90b3f4e47b9fa4cdd8ed53bfb374ba9c93aff2ad Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 16:08:12 +0100 Subject: [PATCH 06/21] fix up printing --- editor/src/components/canvas/canvas-types.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 096cb6f9c4f5..9ec0d41c548f 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -586,6 +586,10 @@ export function maybePropertyValue(property: CSSStyleProperty): T | null { return null } +export interface UntypedStyleInfo { + [cssProperty: string]: CSSStyleProperty | undefined +} + export type FlexGapInfo = CSSStyleProperty export type FlexDirectionInfo = CSSStyleProperty export type LeftInfo = CSSStyleProperty @@ -704,5 +708,40 @@ export function stringifyStyleInfo( printCSSNumber(paddingLeft, null), ), zIndex: mapCSSStyleProperty(styleInfo.zIndex, (zIndex) => printCSSNumber(zIndex, null)), + borderRadius: mapCSSStyleProperty(styleInfo.borderRadius, (borderRadius) => { + switch (borderRadius.type) { + case 'LEFT': + return printCSSNumber(borderRadius.value, null) + case 'RIGHT': + return `${printCSSNumber(borderRadius.value.tl, null)} ${printCSSNumber( + borderRadius.value.tr, + null, + )} ${printCSSNumber(borderRadius.value.br, null)} ${printCSSNumber( + borderRadius.value.bl, + null, + )}` + default: + assertNever(borderRadius) + } + }), + borderTopLeftRadius: mapCSSStyleProperty(styleInfo.borderTopLeftRadius, (borderTopLeftRadius) => + printCSSNumber(borderTopLeftRadius, null), + ), + borderTopRightRadius: mapCSSStyleProperty( + styleInfo.borderTopRightRadius, + (borderTopRightRadius) => printCSSNumber(borderTopRightRadius, null), + ), + borderBottomRightRadius: mapCSSStyleProperty( + styleInfo.borderBottomRightRadius, + (borderBottomRightRadius) => printCSSNumber(borderBottomRightRadius, null), + ), + borderBottomLeftRadius: mapCSSStyleProperty( + styleInfo.borderBottomLeftRadius, + (borderBottomLeftRadius) => printCSSNumber(borderBottomLeftRadius, null), + ), + flexWrap: mapCSSStyleProperty(styleInfo.flexWrap, (flexWrap) => flexWrap), + overflow: mapCSSStyleProperty(styleInfo.overflow, (overflow) => + overflow ? 'visible' : 'hidden', + ), } } From db6781a7a578cb4f6763f329c8db77abff5b3e17 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 16:28:20 +0100 Subject: [PATCH 07/21] encapsulate tailwind class parsing --- .../tailwind-class-list-utils.spec.ts | 32 ++++++++++++++--- .../tailwind/tailwind-class-list-utils.ts | 36 +++++++++++-------- .../core/tailwind/tailwind-parsing-utils.ts | 29 +++++++++++++++ 3 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 editor/src/core/tailwind/tailwind-parsing-utils.ts diff --git a/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts b/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts index ca8816febed2..f222159019b4 100644 --- a/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts +++ b/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts @@ -102,19 +102,43 @@ describe('tailwind class list utils', () => { [ { type: 'parsed', - ast: { property: 'padding', value: '2rem', variants: [], negative: false }, + ast: { + property: 'padding', + value: '2rem', + variants: [], + negative: false, + valueDef: { value: '2rem', class: ['padding'] }, + }, }, { type: 'parsed', - ast: { property: 'positionTop', value: '-14px', variants: [], negative: false }, + ast: { + property: 'positionTop', + value: '-14px', + variants: [], + negative: false, + valueDef: { value: '-14px', class: ['top'] }, + }, }, { type: 'parsed', - ast: { property: 'display', value: 'flex', variants: [], negative: false }, + ast: { + property: 'display', + value: 'flex', + variants: [], + negative: false, + valueDef: { value: 'flex', class: ['display'] }, + }, }, { type: 'parsed', - ast: { property: 'gap', value: '123px', variants: [], negative: false }, + ast: { + property: 'gap', + value: '123px', + variants: [], + negative: false, + valueDef: { value: '123px', class: ['gap'] }, + }, }, { type: 'unparsed', className: 'highlight-button' }, ], diff --git a/editor/src/core/tailwind/tailwind-class-list-utils.ts b/editor/src/core/tailwind/tailwind-class-list-utils.ts index fd6152c79f32..452e984a74d3 100644 --- a/editor/src/core/tailwind/tailwind-class-list-utils.ts +++ b/editor/src/core/tailwind/tailwind-class-list-utils.ts @@ -1,13 +1,10 @@ -import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' import type { Config } from 'tailwindcss/types/config' import { mapDropNulls } from '../shared/array-utils' - -export type ParsedTailwindClass = { - property: string - value: string - variants: unknown[] - negative: boolean -} & Record +import { + getTailwindClassName, + parseTailwindClass, + type ParsedTailwindClass, +} from './tailwind-parsing-utils' export type TailwindClassParserResult = | { type: 'unparsed'; className: string } @@ -18,7 +15,7 @@ export function getParsedClassList( config: Config | null, ): TailwindClassParserResult[] { return classList.split(' ').map((c) => { - const result = TailwindClassParser.parse(c, config ?? undefined) + const result = parseTailwindClass(c, config) if (result.kind === 'error') { return { type: 'unparsed', className: c } } @@ -35,10 +32,7 @@ export function getClassListFromParsedClassList( if (c.type === 'unparsed') { return c.className } - return TailwindClassParser.classname( - c.ast as any, // FIXME the types are not exported from @xengine/tailwindcss-class-parser - config ?? undefined, - ) + return getTailwindClassName(c.ast, config) }) .filter((part) => part != null && part.length > 0) .join(' ') @@ -65,7 +59,13 @@ export const addNewClasses = ? null : { type: 'parsed', - ast: { property: prop, value: value, variants: [], negative: false }, + ast: { + property: prop, + value: value, + variants: [], + negative: false, + valueDef: { value: value, class: [] }, // dummy values + }, }, Object.entries(propertiesToAdd), ) @@ -87,7 +87,13 @@ export const updateExistingClasses = } return { type: 'parsed', - ast: { property: cls.ast.property, value: updatedProperty, variants: [], negative: false }, + ast: { + property: cls.ast.property, + value: updatedProperty, + variants: [], + negative: false, + valueDef: cls.ast.valueDef, + }, } }) return classListWithUpdatedClasses diff --git a/editor/src/core/tailwind/tailwind-parsing-utils.ts b/editor/src/core/tailwind/tailwind-parsing-utils.ts new file mode 100644 index 000000000000..d86d37a46ed4 --- /dev/null +++ b/editor/src/core/tailwind/tailwind-parsing-utils.ts @@ -0,0 +1,29 @@ +import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' +import type { Config } from 'tailwindcss' + +type ParsedTailwindClassVariant = { value: string } & Record +type ParsedTailwindValueDef = { value: string; class: Array } & Record + +export type ParsedTailwindClass = { + property: string + value: string + variants: Array + negative: boolean + valueDef: ParsedTailwindValueDef +} & Record + +export function parseTailwindClass(className: string, config: Config | null) { + try { + return TailwindClassParser.parse(className, config ?? undefined) + } catch (e) { + return { kind: 'error', error: e } + } +} + +export function getTailwindClassName(parsedClass: ParsedTailwindClass, config: Config | null) { + try { + return TailwindClassParser.classname(parsedClass, config) + } catch { + return '' + } +} From a2e0611304291c7b33e95b47ac4c9b9b82e34c6a Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 16:48:19 +0100 Subject: [PATCH 08/21] untyped style info from inline styles --- .../canvas/plugins/inline-style-plugin.ts | 129 ++++++++++++------ .../canvas/plugins/style-plugins.ts | 8 +- .../canvas/plugins/tailwind-style-plugin.ts | 20 +-- 3 files changed, 105 insertions(+), 52 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index d8c71ec114dc..7494a9e54031 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -1,14 +1,12 @@ -import type { JSXAttributes, PropertyPath } from 'utopia-shared/src/types' +import type { JSXAttributes } from 'utopia-shared/src/types' import * as Either from '../../../core/shared/either' import { getJSXAttributesAtPath, jsxSimpleAttributeToValue, } from '../../../core/shared/jsx-attribute-utils' -import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes' import { getJSXElementFromProjectContents } from '../../editor/store/editor-state' import { cssParsers, type ParsedCSSProperties } from '../../inspector/common/css-utils' -import { stylePropPathMappingFn } from '../../inspector/common/property-path-hooks' -import type { CSSStyleProperty, StyleInfo } from '../canvas-types' +import type { CSSStyleProperty, StyleInfo, UntypedStyleInfo } from '../canvas-types' import { cssStyleProperty, cssStylePropertyNotParsable, @@ -20,79 +18,126 @@ import * as PP from '../../../core/shared/property-path' import { applyValuesAtPath, deleteValuesAtPath } from '../commands/utils/property-utils' import type { StylePlugin } from './style-plugins' -function getPropValue(attributes: JSXAttributes, path: PropertyPath): ModifiableAttribute { - const result = getJSXAttributesAtPath(attributes, path) - if (result.remainingPath != null) { - return { type: 'ATTRIBUTE_NOT_FOUND' } +function getUntypedStyleInfo(jsxElementProps: JSXAttributes): UntypedStyleInfo | null { + const styleProp = getJSXAttributesAtPath(jsxElementProps, PP.create('style')) + if ( + styleProp.attribute.type === 'ATTRIBUTE_NOT_FOUND' || + styleProp.remainingPath != null || + styleProp.attribute.type === 'PART_OF_ATTRIBUTE_VALUE' + ) { + return null } - return result.attribute + + if ( + styleProp.attribute.type === 'ATTRIBUTE_VALUE' && + typeof styleProp.attribute.value === 'object' + ) { + return styleProp.attribute.value + } + + if (styleProp.attribute.type === 'ATTRIBUTE_NESTED_OBJECT') { + let result: UntypedStyleInfo = {} + styleProp.attribute.content.forEach((assignment) => { + if (assignment.type === 'SPREAD_ASSIGNMENT') { + return + } + + if (typeof assignment.key !== 'string') { + return + } + + if (assignment.value.type !== 'ATTRIBUTE_VALUE') { + result[assignment.key] = cssStylePropertyNotParsable(assignment.value) + return + } + + result[assignment.key] = cssStyleProperty(assignment.value.value, assignment.value) + }) + return result + } + + return null } function getPropertyFromInstance

( prop: P, - attributes: JSXAttributes, + untypedStyleInfo: UntypedStyleInfo, ): CSSStyleProperty> | null { - const attribute = getPropValue(attributes, stylePropPathMappingFn(prop, ['style'])) - if (attribute.type === 'ATTRIBUTE_NOT_FOUND') { + const attribute = untypedStyleInfo[prop] + if (attribute === undefined || attribute.type === 'not-found') { return cssStylePropertyNotFound() } - const simpleValue = jsxSimpleAttributeToValue(attribute) + if (attribute.type === 'not-parsable') { + return attribute + } + const simpleValue = jsxSimpleAttributeToValue(attribute.propertyValue) if (Either.isLeft(simpleValue)) { - return cssStylePropertyNotParsable(attribute) + return cssStylePropertyNotParsable(attribute.propertyValue) } const parser = cssParsers[prop] as (value: unknown) => Either.Either const parsed = parser(simpleValue.value) if (Either.isLeft(parsed) || parsed.value == null) { - return cssStylePropertyNotParsable(attribute) + return cssStylePropertyNotParsable(attribute.propertyValue) } - return cssStyleProperty(parsed.value, attribute) + return cssStyleProperty(parsed.value, attribute.propertyValue) } export const InlineStylePlugin: StylePlugin = { name: 'Inline Style', + readUntypedStyleInfo: (projectContents, elementPath) => { + const element = getJSXElementFromProjectContents(elementPath, projectContents) + if (element == null) { + return null + } + + return getUntypedStyleInfo(element.props) + }, readStyleFromElementProps: ( attributes: JSXAttributes, prop: T, ): CSSStyleProperty> | null => { - return getPropertyFromInstance(prop, attributes) + const untypedStyleInfo = getUntypedStyleInfo(attributes) + if (untypedStyleInfo == null) { + return null + } + return getPropertyFromInstance(prop, untypedStyleInfo) }, styleInfoFactory: ({ projectContents }) => (elementPath) => { - const element = getJSXElementFromProjectContents(elementPath, projectContents) - if (element == null) { + const untypedStyleInfo = InlineStylePlugin.readUntypedStyleInfo(projectContents, elementPath) + if (untypedStyleInfo == null) { return null } - const gap = getPropertyFromInstance('gap', element.props) - const flexDirection = getPropertyFromInstance('flexDirection', element.props) - const left = getPropertyFromInstance('left', element.props) - const right = getPropertyFromInstance('right', element.props) - const top = getPropertyFromInstance('top', element.props) - const bottom = getPropertyFromInstance('bottom', element.props) - const width = getPropertyFromInstance('width', element.props) - const height = getPropertyFromInstance('height', element.props) - const flexBasis = getPropertyFromInstance('flexBasis', element.props) - const padding = getPropertyFromInstance('padding', element.props) - const paddingTop = getPropertyFromInstance('paddingTop', element.props) - 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 gap = getPropertyFromInstance('gap', untypedStyleInfo) + const flexDirection = getPropertyFromInstance('flexDirection', untypedStyleInfo) + const left = getPropertyFromInstance('left', untypedStyleInfo) + const right = getPropertyFromInstance('right', untypedStyleInfo) + const top = getPropertyFromInstance('top', untypedStyleInfo) + const bottom = getPropertyFromInstance('bottom', untypedStyleInfo) + const width = getPropertyFromInstance('width', untypedStyleInfo) + const height = getPropertyFromInstance('height', untypedStyleInfo) + const flexBasis = getPropertyFromInstance('flexBasis', untypedStyleInfo) + const padding = getPropertyFromInstance('padding', untypedStyleInfo) + const paddingTop = getPropertyFromInstance('paddingTop', untypedStyleInfo) + const paddingBottom = getPropertyFromInstance('paddingBottom', untypedStyleInfo) + const paddingLeft = getPropertyFromInstance('paddingLeft', untypedStyleInfo) + const paddingRight = getPropertyFromInstance('paddingRight', untypedStyleInfo) + const borderRadius = getPropertyFromInstance('borderRadius', untypedStyleInfo) + const borderTopLeftRadius = getPropertyFromInstance('borderTopLeftRadius', untypedStyleInfo) + const borderTopRightRadius = getPropertyFromInstance('borderTopRightRadius', untypedStyleInfo) const borderBottomRightRadius = getPropertyFromInstance( 'borderBottomRightRadius', - element.props, + untypedStyleInfo, ) const borderBottomLeftRadius = getPropertyFromInstance( 'borderBottomLeftRadius', - element.props, + untypedStyleInfo, ) - const zIndex = getPropertyFromInstance('zIndex', element.props) - const flexWrap = getPropertyFromInstance('flexWrap', element.props) - - const overflow = getPropertyFromInstance('overflow', element.props) + const zIndex = getPropertyFromInstance('zIndex', untypedStyleInfo) + const flexWrap = getPropertyFromInstance('flexWrap', untypedStyleInfo) + const overflow = getPropertyFromInstance('overflow', untypedStyleInfo) return { gap: gap, diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index b99618528b5b..f9cff673280d 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -1,4 +1,4 @@ -import type { ElementPath, JSXAttributes } from 'utopia-shared/src/types' +import type { ElementPath, JSXAttributes, ProjectContentTreeRoot } from 'utopia-shared/src/types' import type { EditorState, EditorStatePatch } from '../../editor/store/editor-state' import type { InteractionLifecycle, @@ -18,7 +18,7 @@ import type { EditorStateWithPatch } from '../commands/utils/property-utils' import { applyValuesAtPath } from '../commands/utils/property-utils' import * as PP from '../../../core/shared/property-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' -import type { CSSStyleProperty } from '../canvas-types' +import type { CSSStyleProperty, UntypedStyleInfo } 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' @@ -58,6 +58,10 @@ export interface StylePlugin { attributes: JSXAttributes, prop: T, ) => CSSStyleProperty> | null + readUntypedStyleInfo: ( + projectContents: ProjectContentTreeRoot, + elementPath: ElementPath, + ) => UntypedStyleInfo | null updateStyles: ( editorState: EditorState, elementPath: ElementPath, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 84c374b6ed20..b6e4a8de3147 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,4 +1,3 @@ -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' @@ -18,6 +17,7 @@ import { } from '../../../core/shared/jsx-attribute-utils' import * as PP from '../../../core/shared/property-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' +import { getParsedClassList } from '../../../core/tailwind/tailwind-class-list-utils' const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') @@ -87,15 +87,18 @@ function stringifyPropertyValue(value: string | number): string { } } -function getTailwindClassMapping(classes: string[], config: Config | null): Record { +function getTailwindClassMapping( + classNameAttribute: string, + config: Config | null, +): Record { + const classes = getParsedClassList(classNameAttribute, config) const mapping: Record = {} classes.forEach((className) => { - const parsed = TailwindClassParser.parse(className, config ?? undefined) - if (parsed.kind === 'error') { + if (className.type === 'unparsed') { return } - parsed.valueDef.class.forEach((cls: string) => { - mapping[toCamelCase(cls)] = parsed.value + className.ast.valueDef.class.forEach((cls: string) => { + mapping[toCamelCase(cls)] = className.ast.valueDef.value }) }) return mapping @@ -103,6 +106,7 @@ function getTailwindClassMapping(classes: string[], config: Config | null): Reco export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', + readUntypedStyleInfo: () => null, // TODO readStyleFromElementProps:

( attributes: JSXAttributes, prop: P, @@ -119,7 +123,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return null } - const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) + const mapping = getTailwindClassMapping(classNameAttribute, config) return parseTailwindProperty(mapping, prop) }, styleInfoFactory: @@ -133,7 +137,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ return null } - const mapping = getTailwindClassMapping(classList.split(' '), config) + const mapping = getTailwindClassMapping(classList, config) return { gap: parseTailwindProperty(mapping, 'gap'), From 5f9332a93cd57f37b7f0a21413a76865f71e97fc Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 17:09:59 +0100 Subject: [PATCH 09/21] untyped style info for tailwind --- .../canvas/plugins/tailwind-style-plugin.ts | 126 +++++++++++------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index b6e4a8de3147..2f14e8c83cb7 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,31 +1,38 @@ -import { defaultEither, flatMapEither, isLeft } from '../../../core/shared/either' -import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' -import { getElementFromProjectContents } from '../../editor/store/editor-state' +import { getJSXElementFromProjectContents } from '../../editor/store/editor-state' 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 type { StyleInfo, UntypedStyleInfo } 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 type { JSXAttributes } from 'utopia-shared/src/types' -import { - jsxSimpleAttributeToValue, - getModifiableJSXAttributeAtPath, -} from '../../../core/shared/jsx-attribute-utils' +import { getModifiableJSXAttributeAtPath } from '../../../core/shared/jsx-attribute-utils' import * as PP from '../../../core/shared/property-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' import { getParsedClassList } from '../../../core/tailwind/tailwind-class-list-utils' +import { isLeft } from '../../../core/shared/either' const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') function parseTailwindProperty

( - mapping: Record, + mapping: UntypedStyleInfo, prop: P, ): CSSStyleProperty> | null { - const value = prop === 'padding' ? underscoresToSpaces(mapping[prop]) : mapping[prop] + const property = mapping[prop] + if (property == null) { + return null + } + if (property.type === 'not-found' || property.type === 'not-parsable') { + return property + } + + const value = + prop === 'padding' && typeof property.value === 'string' + ? underscoresToSpaces(property.value) + : mapping[prop] const parsed = cssParsers[prop](value, null) if (isLeft(parsed) || parsed.value == null) { return null @@ -104,64 +111,81 @@ function getTailwindClassMapping( return mapping } +function getUntypedStyleInfoFromAttributes( + attributes: JSXAttributes, + config: Config | null, +): UntypedStyleInfo | null { + const classNameAttribute = getModifiableJSXAttributeAtPath(attributes, PP.create('className')) + if ( + classNameAttribute.type === 'LEFT' || + classNameAttribute.value.type !== 'ATTRIBUTE_VALUE' || + typeof classNameAttribute.value.value !== 'string' + ) { + return null + } + + const mapping = getTailwindClassMapping(classNameAttribute.value.value, config) + const result: UntypedStyleInfo = {} + Object.entries(mapping).forEach(([key, value]) => { + result[key] = cssStyleProperty(value, jsExpressionValue(value, emptyComments)) + }) + return result +} + export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', - readUntypedStyleInfo: () => null, // TODO + readUntypedStyleInfo: (projectContents, elementPath) => { + const element = getJSXElementFromProjectContents(elementPath, projectContents) + if (element == null) { + return null + } + return getUntypedStyleInfoFromAttributes(element.props, config) + }, readStyleFromElementProps:

( attributes: JSXAttributes, prop: P, ): CSSStyleProperty> | null => { - const classNameAttribute = defaultEither( - null, - flatMapEither( - (attr) => jsxSimpleAttributeToValue(attr), - getModifiableJSXAttributeAtPath(attributes, PP.create('className')), - ), - ) - - if (typeof classNameAttribute !== 'string') { + const mapping = getUntypedStyleInfoFromAttributes(attributes, config) + if (mapping == null) { return null } - - const mapping = getTailwindClassMapping(classNameAttribute, config) return parseTailwindProperty(mapping, prop) }, styleInfoFactory: ({ projectContents }) => (elementPath) => { - const classList = getClassNameAttribute( - getElementFromProjectContents(elementPath, projectContents), - )?.value - - if (classList == null || typeof classList !== 'string') { + const element = getJSXElementFromProjectContents(elementPath, projectContents) + if (element == null) { + return null + } + const untypedStyleInfo = getUntypedStyleInfoFromAttributes(element.props, config) + if (untypedStyleInfo == null) { return null } - - const mapping = getTailwindClassMapping(classList, config) return { - gap: parseTailwindProperty(mapping, 'gap'), - flexDirection: parseTailwindProperty(mapping, 'flexDirection'), - left: parseTailwindProperty(mapping, 'left'), - right: parseTailwindProperty(mapping, 'right'), - top: parseTailwindProperty(mapping, 'top'), - bottom: parseTailwindProperty(mapping, 'bottom'), - width: parseTailwindProperty(mapping, 'width'), - height: parseTailwindProperty(mapping, 'height'), - flexBasis: parseTailwindProperty(mapping, 'flexBasis'), - padding: parseTailwindProperty(mapping, 'padding'), - paddingTop: parseTailwindProperty(mapping, 'paddingTop'), - paddingRight: parseTailwindProperty(mapping, 'paddingRight'), - paddingBottom: parseTailwindProperty(mapping, 'paddingBottom'), - paddingLeft: parseTailwindProperty(mapping, 'paddingLeft'), - zIndex: parseTailwindProperty(mapping, 'zIndex'), - borderRadius: parseTailwindProperty(mapping, 'borderRadius'), - borderTopLeftRadius: parseTailwindProperty(mapping, 'borderTopLeftRadius'), - borderTopRightRadius: parseTailwindProperty(mapping, 'borderTopRightRadius'), - borderBottomRightRadius: parseTailwindProperty(mapping, 'borderBottomRightRadius'), - borderBottomLeftRadius: parseTailwindProperty(mapping, 'borderBottomLeftRadius'), - flexWrap: parseTailwindProperty(mapping, 'flexWrap'), - overflow: parseTailwindProperty(mapping, 'overflow'), + gap: parseTailwindProperty(untypedStyleInfo, 'gap'), + flexDirection: parseTailwindProperty(untypedStyleInfo, 'flexDirection'), + left: parseTailwindProperty(untypedStyleInfo, 'left'), + right: parseTailwindProperty(untypedStyleInfo, 'right'), + top: parseTailwindProperty(untypedStyleInfo, 'top'), + bottom: parseTailwindProperty(untypedStyleInfo, 'bottom'), + width: parseTailwindProperty(untypedStyleInfo, 'width'), + height: parseTailwindProperty(untypedStyleInfo, 'height'), + flexBasis: parseTailwindProperty(untypedStyleInfo, 'flexBasis'), + padding: parseTailwindProperty(untypedStyleInfo, 'padding'), + paddingTop: parseTailwindProperty(untypedStyleInfo, 'paddingTop'), + paddingRight: parseTailwindProperty(untypedStyleInfo, 'paddingRight'), + paddingBottom: parseTailwindProperty(untypedStyleInfo, 'paddingBottom'), + paddingLeft: parseTailwindProperty(untypedStyleInfo, 'paddingLeft'), + zIndex: parseTailwindProperty(untypedStyleInfo, 'zIndex'), + borderRadius: parseTailwindProperty(untypedStyleInfo, 'borderRadius'), + borderTopLeftRadius: parseTailwindProperty(untypedStyleInfo, 'borderTopLeftRadius'), + borderTopRightRadius: parseTailwindProperty(untypedStyleInfo, 'borderTopRightRadius'), + borderBottomRightRadius: parseTailwindProperty(untypedStyleInfo, 'borderBottomRightRadius'), + borderBottomLeftRadius: parseTailwindProperty(untypedStyleInfo, 'borderBottomLeftRadius'), + flexWrap: parseTailwindProperty(untypedStyleInfo, 'flexWrap'), + overflow: parseTailwindProperty(untypedStyleInfo, 'overflow'), } }, updateStyles: (editorState, elementPath, updates) => { From 31271211cb960becffaa7e8597a52f274a756785 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 17:22:44 +0100 Subject: [PATCH 10/21] fix forbidden import problem --- editor/src/components/canvas/canvas-types.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 9ec0d41c548f..ae5c4b9c119d 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -22,7 +22,6 @@ import type { } from './canvas-strategies/interaction-state' import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types' import type { MouseButtonsPressed } from '../../utils/mouse' -import { printCSSNumber } from '../inspector/common/css-utils' import type { FlexWrap } from 'utopia-api/core' import type { CSSNumber, @@ -669,6 +668,15 @@ function mapCSSStyleProperty( return map(property.value) } +function printCSSNumber(input: CSSNumber, defaultUnitToSkip: string | null): string | number { + const { value, unit } = input + if (unit == null || unit === defaultUnitToSkip) { + return Number(value.toFixed(11)) + } else { + return `${Number(value.toFixed(11))}${unit}` + } +} + export function stringifyStyleInfo( styleInfo: StyleInfo, ): Record { From 022a3ae621f65a467074f9f929bd789d688ca4aa Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 25 Nov 2024 18:14:18 +0100 Subject: [PATCH 11/21] same but well typed --- editor/src/components/canvas/canvas-types.ts | 2 +- .../canvas/plugins/inline-style-plugin.ts | 21 +++++++++++-------- .../canvas/plugins/tailwind-style-plugin.ts | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index ae5c4b9c119d..81a7b3f5d0dd 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -586,7 +586,7 @@ export function maybePropertyValue(property: CSSStyleProperty): T | null { } export interface UntypedStyleInfo { - [cssProperty: string]: CSSStyleProperty | undefined + [cssProperty: string]: CSSStylePropertyNotParsable | ParsedCSSStyleProperty | undefined } export type FlexGapInfo = CSSStyleProperty diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 7494a9e54031..71f3d65b08fd 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -32,7 +32,11 @@ function getUntypedStyleInfo(jsxElementProps: JSXAttributes): UntypedStyleInfo | styleProp.attribute.type === 'ATTRIBUTE_VALUE' && typeof styleProp.attribute.value === 'object' ) { - return styleProp.attribute.value + let result: UntypedStyleInfo = {} + for (const [key, value] of Object.entries(styleProp.attribute.value)) { + result[key] = cssStyleProperty(value, jsExpressionValue(value, emptyComments)) + } + return result } if (styleProp.attribute.type === 'ATTRIBUTE_NESTED_OBJECT') { @@ -64,22 +68,21 @@ function getPropertyFromInstance

> | null { const attribute = untypedStyleInfo[prop] - if (attribute === undefined || attribute.type === 'not-found') { + if (attribute == null) { return cssStylePropertyNotFound() } - if (attribute.type === 'not-parsable') { - return attribute - } - const simpleValue = jsxSimpleAttributeToValue(attribute.propertyValue) + const expression = + attribute.type === 'not-parsable' ? attribute.originalValue : attribute.propertyValue + const simpleValue = jsxSimpleAttributeToValue(expression) if (Either.isLeft(simpleValue)) { - return cssStylePropertyNotParsable(attribute.propertyValue) + return cssStylePropertyNotParsable(expression) } const parser = cssParsers[prop] as (value: unknown) => Either.Either const parsed = parser(simpleValue.value) if (Either.isLeft(parsed) || parsed.value == null) { - return cssStylePropertyNotParsable(attribute.propertyValue) + return cssStylePropertyNotParsable(expression) } - return cssStyleProperty(parsed.value, attribute.propertyValue) + return cssStyleProperty(parsed.value, expression) } export const InlineStylePlugin: StylePlugin = { diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 2f14e8c83cb7..45e76d2e2eea 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -25,7 +25,7 @@ function parseTailwindProperty

( if (property == null) { return null } - if (property.type === 'not-found' || property.type === 'not-parsable') { + if (property.type === 'not-parsable') { return property } From 4f9082e5f638f73f47041cef99412cbb7e2d04fe Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 11:47:01 +0100 Subject: [PATCH 12/21] use untyped style info in conversion --- editor/src/components/canvas/canvas-types.ts | 96 ------------------- ...nline-style-tailwind-conversion-command.ts | 30 +++--- .../canvas/plugins/tailwind-style-plugin.ts | 3 + editor/src/components/context-menu-items.ts | 8 +- 4 files changed, 24 insertions(+), 113 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 81a7b3f5d0dd..995fef5abe7a 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -657,99 +657,3 @@ const emptyStyleInfo: StyleInfo = { } export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo - -function mapCSSStyleProperty( - property: CSSStyleProperty | null, - map: (value: T) => U, -): U | null { - if (property === null || property.type !== 'property') { - return null - } - return map(property.value) -} - -function printCSSNumber(input: CSSNumber, defaultUnitToSkip: string | null): string | number { - const { value, unit } = input - if (unit == null || unit === defaultUnitToSkip) { - return Number(value.toFixed(11)) - } else { - return `${Number(value.toFixed(11))}${unit}` - } -} - -export function stringifyStyleInfo( - styleInfo: StyleInfo, -): Record { - return { - gap: mapCSSStyleProperty(styleInfo.gap, (gap) => printCSSNumber(gap, null)), - flexDirection: mapCSSStyleProperty(styleInfo.flexDirection, (flexDirection) => flexDirection), - left: mapCSSStyleProperty(styleInfo.left, (left) => printCSSNumber(left, null)), - right: mapCSSStyleProperty(styleInfo.right, (right) => printCSSNumber(right, null)), - top: mapCSSStyleProperty(styleInfo.top, (top) => printCSSNumber(top, null)), - bottom: mapCSSStyleProperty(styleInfo.bottom, (bottom) => printCSSNumber(bottom, null)), - width: mapCSSStyleProperty(styleInfo.width, (width) => printCSSNumber(width, null)), - height: mapCSSStyleProperty(styleInfo.height, (height) => printCSSNumber(height, null)), - flexBasis: mapCSSStyleProperty(styleInfo.flexBasis, (flexBasis) => - printCSSNumber(flexBasis, null), - ), - padding: mapCSSStyleProperty( - styleInfo.padding, - (padding) => - `${printCSSNumber(padding.paddingTop, null)} ${printCSSNumber( - padding.paddingRight, - null, - )} ${printCSSNumber(padding.paddingBottom, null)} ${printCSSNumber( - padding.paddingLeft, - null, - )}`, - ), - paddingTop: mapCSSStyleProperty(styleInfo.paddingTop, (paddingTop) => - printCSSNumber(paddingTop, null), - ), - paddingRight: mapCSSStyleProperty(styleInfo.paddingRight, (paddingRight) => - printCSSNumber(paddingRight, null), - ), - paddingBottom: mapCSSStyleProperty(styleInfo.paddingBottom, (paddingBottom) => - printCSSNumber(paddingBottom, null), - ), - paddingLeft: mapCSSStyleProperty(styleInfo.paddingLeft, (paddingLeft) => - printCSSNumber(paddingLeft, null), - ), - zIndex: mapCSSStyleProperty(styleInfo.zIndex, (zIndex) => printCSSNumber(zIndex, null)), - borderRadius: mapCSSStyleProperty(styleInfo.borderRadius, (borderRadius) => { - switch (borderRadius.type) { - case 'LEFT': - return printCSSNumber(borderRadius.value, null) - case 'RIGHT': - return `${printCSSNumber(borderRadius.value.tl, null)} ${printCSSNumber( - borderRadius.value.tr, - null, - )} ${printCSSNumber(borderRadius.value.br, null)} ${printCSSNumber( - borderRadius.value.bl, - null, - )}` - default: - assertNever(borderRadius) - } - }), - borderTopLeftRadius: mapCSSStyleProperty(styleInfo.borderTopLeftRadius, (borderTopLeftRadius) => - printCSSNumber(borderTopLeftRadius, null), - ), - borderTopRightRadius: mapCSSStyleProperty( - styleInfo.borderTopRightRadius, - (borderTopRightRadius) => printCSSNumber(borderTopRightRadius, null), - ), - borderBottomRightRadius: mapCSSStyleProperty( - styleInfo.borderBottomRightRadius, - (borderBottomRightRadius) => printCSSNumber(borderBottomRightRadius, null), - ), - borderBottomLeftRadius: mapCSSStyleProperty( - styleInfo.borderBottomLeftRadius, - (borderBottomLeftRadius) => printCSSNumber(borderBottomLeftRadius, null), - ), - flexWrap: mapCSSStyleProperty(styleInfo.flexWrap, (flexWrap) => flexWrap), - overflow: mapCSSStyleProperty(styleInfo.overflow, (overflow) => - overflow ? 'visible' : 'hidden', - ), - } -} diff --git a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts index 24614c9c1941..7066889fb0b9 100644 --- a/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts +++ b/editor/src/components/canvas/commands/inline-style-tailwind-conversion-command.ts @@ -3,8 +3,7 @@ import type { BaseCommand, CommandFunctionResult } from './commands' import type { EditorState, EditorStatePatch } from '../../editor/store/editor-state' import type { DeleteCSSProp, EditorStateWithPatches, UpdateCSSProp } from '../plugins/style-plugins' import { InlineStylePlugin } from '../plugins/inline-style-plugin' -import type { StyleInfo } from '../canvas-types' -import { stringifyStyleInfo } from '../canvas-types' +import type { UntypedStyleInfo } from '../canvas-types' import { TailwindPlugin } from '../plugins/tailwind-style-plugin' import { getTailwindConfigCached } from '../../../core/tailwind/tailwind-compilation' import { mapDropNulls } from '../../../core/shared/array-utils' @@ -30,21 +29,23 @@ export function inlineStyleTailwindConversionCommand( } } -function getStyleInfoUpdates(styleInfo: StyleInfo): { +const stringify = (value: unknown): string => + typeof value === 'string' ? value : typeof value === 'number' ? `${value}px` : `${value}` + +function getStyleUpdates(untypedStyleInfo: UntypedStyleInfo): { stylesToAdd: UpdateCSSProp[] stylesToRemove: DeleteCSSProp[] } { - const styleInfoString = stringifyStyleInfo(styleInfo) const stylesToAdd: UpdateCSSProp[] = mapDropNulls( ([property, value]) => - value == null + value?.type !== 'property' ? null : { property: property, - value: value, + value: stringify(value.value), type: 'set', }, - Object.entries(styleInfoString), + Object.entries(untypedStyleInfo), ) const stylesToRemove: DeleteCSSProp[] = stylesToAdd.map((style) => ({ @@ -63,15 +64,16 @@ function convertInlineStyleToTailwindViaStyleInfo( let editorStateWithChanges: EditorState = editorState elementPaths.forEach((elementPath) => { - const styleInfo = InlineStylePlugin.styleInfoFactory({ - projectContents: editorState.projectContents, - })(elementPath) + const styleInfo = InlineStylePlugin.readUntypedStyleInfo( + editorState.projectContents, + elementPath, + ) if (styleInfo == null) { return } - const { stylesToAdd, stylesToRemove } = getStyleInfoUpdates(styleInfo) + const { stylesToAdd, stylesToRemove } = getStyleUpdates(styleInfo) const { editorStateWithChanges: updatedEditorState } = TailwindPlugin( getTailwindConfigCached(editorStateWithChanges), @@ -100,15 +102,13 @@ function convertTailwindToInlineStyleViaStyleInfo( elementPaths.forEach((elementPath) => { const styleInfo = TailwindPlugin( getTailwindConfigCached(editorStateWithChanges), - ).styleInfoFactory({ - projectContents: editorStateWithChanges.projectContents, - })(elementPath) + ).readUntypedStyleInfo(editorState.projectContents, elementPath) if (styleInfo == null) { return } - const { stylesToAdd, stylesToRemove } = getStyleInfoUpdates(styleInfo) + const { stylesToAdd, stylesToRemove } = getStyleUpdates(styleInfo) const { editorStateWithChanges: updatedEditorState } = InlineStylePlugin.updateStyles( editorStateWithChanges, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 45e76d2e2eea..e51cffc9c8c9 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -45,6 +45,7 @@ const TailwindPropertyMapping: Record = { right: 'positionRight', top: 'positionTop', bottom: 'positionBottom', + position: 'position', width: 'width', height: 'height', @@ -74,6 +75,8 @@ const TailwindPropertyMapping: Record = { overflow: 'overflow', zIndex: 'zIndex', + + backgroundColor: 'backgroundColor', } function toCamelCase(str: string): string { diff --git a/editor/src/components/context-menu-items.ts b/editor/src/components/context-menu-items.ts index 6cda79d418d5..06fc1ae5fa2c 100644 --- a/editor/src/components/context-menu-items.ts +++ b/editor/src/components/context-menu-items.ts @@ -565,8 +565,10 @@ export const escapeHatch: ContextMenuItem = { }, } +export const ConvertInlineStyleToTailwindOptionText = 'Convert Inline Style to Tailwind Style' + export const convertInlineStyleToTailwindStyle: ContextMenuItem = { - name: 'Convert Inline Style to Tailwind Style', + name: ConvertInlineStyleToTailwindOptionText, enabled: true, action: (data, dispatch?: EditorDispatch) => { dispatch?.([ @@ -577,8 +579,10 @@ export const convertInlineStyleToTailwindStyle: ContextMenuItem = { }, } +export const ConvertTailwindToInlineStyleOptionText = 'Convert Tailwind Style to Inline Style' + export const convertTailwindStyleToInlineStyle: ContextMenuItem = { - name: 'Convert Tailwind Style to Inline Style', + name: ConvertTailwindToInlineStyleOptionText, enabled: true, action: (data, dispatch?: EditorDispatch) => { dispatch?.([ From b3d2d4945a1ffdd47bf7957b994b8389085df58b Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 11:47:19 +0100 Subject: [PATCH 13/21] tests --- .../canvas-context-menu.spec.browser2.tsx | 173 +++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx index 72862fa9e296..6761cad5edd5 100644 --- a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx @@ -8,7 +8,7 @@ import { BakedInStoryboardUID } from '../../core/model/scene-utils' import * as EP from '../../core/shared/element-path' import { altCmdModifier, cmdModifier } from '../../utils/modifiers' import { selectComponents } from '../editor/actions/meta-actions' -import { navigatorEntryToKey } from '../editor/store/editor-state' +import { navigatorEntryToKey, StoryboardFilePath } from '../editor/store/editor-state' import { CanvasControlsContainerID } from './controls/new-canvas-controls' import { mouseClickAtPoint, @@ -22,6 +22,7 @@ import { makeTestProjectCodeWithSnippet, makeTestProjectCodeWithSnippetWithoutUIDs, renderTestEditorWithCode, + renderTestEditorWithModel, TestAppUID, TestSceneUID, } from './ui-jsx.test-utils' @@ -29,11 +30,18 @@ import { expectNoAction, searchInComponentPicker, selectComponentsForTest, + setFeatureForBrowserTestsUseInDescribeBlockOnly, } from '../../utils/utils.test-utils' import { MetadataUtils } from '../../core/model/element-metadata-utils' import type { ElementPath } from '../../core/shared/project-file-types' import { getDomRectCenter } from '../../core/shared/dom-utils' import { getNavigatorTargetsFromEditorState } from '../navigator/navigator-utils' +import { + ConvertInlineStyleToTailwindOptionText, + ConvertTailwindToInlineStyleOptionText, +} from '../context-menu-items' +import { createModifiedProject } from '../../sample-projects/sample-project-utils.test-utils' +import { TailwindConfigPath } from '../../core/tailwind/tailwind-config' function expectAllSelectedViewsToHaveMetadata(editor: EditorRenderResult) { const selectedViews = editor.getEditorState().editor.selectedViews @@ -969,8 +977,171 @@ describe('canvas context menu', () => { ) }) }) + + describe('Inline style <> Tailwind conversion', () => { + setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) + it('can convert element styles from Tailwind to inline style', async () => { + const editor = await renderTestEditorWithModel( + createModifiedProject({ + [StoryboardFilePath]: `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = (props) => { + return ( + + +

+ + + ) +}`, + [TailwindConfigPath]: ` + const TailwindConfig = { } + export default TailwindConfig + `, + 'app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities;`, + }), + 'await-first-dom-report', + ) + + await openContextMenuOnElement(editor, { + testId: 'bbb', + contextMenuItemLabel: ConvertTailwindToInlineStyleOptionText, + }) + + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(`import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = (props) => { + return ( + + +
+ + + ) +} +`) + }) + + it('can convert element styles from inline style to Tailwind', async () => { + const editor = await renderTestEditorWithModel( + createModifiedProject({ + [StoryboardFilePath]: `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = (props) => { + return ( + + +
+ + + ) +}`, + [TailwindConfigPath]: ` + const TailwindConfig = { } + export default TailwindConfig + `, + 'app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities;`, + }), + 'await-first-dom-report', + ) + + await openContextMenuOnElement(editor, { + testId: 'bbb', + contextMenuItemLabel: ConvertInlineStyleToTailwindOptionText, + }) + + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(`import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = (props) => { + return ( + + +
+ + + ) +} +`) + }) + }) }) +async function openContextMenuOnElement( + editor: EditorRenderResult, + { testId, contextMenuItemLabel }: { testId: string; contextMenuItemLabel: string }, +) { + const canvasControlsLayer = editor.renderedDOM.getByTestId(CanvasControlsContainerID) + const element = editor.renderedDOM.getByTestId(testId) + const elementCenter = getDomRectCenter(element.getBoundingClientRect()) + await mouseClickAtPoint(canvasControlsLayer, elementCenter) + await editor.getDispatchFollowUpActionsFinished() + + await openContextMenuAndClickOnItem( + editor, + canvasControlsLayer, + elementCenter, + contextMenuItemLabel, + ) + await editor.getDispatchFollowUpActionsFinished() +} + async function wrapInElement(renderResult: EditorRenderResult, query: string) { await pressKey('w') // open the wrap menu await searchInComponentPicker(renderResult, query) From f74c8f6aab1dba2d0ddbc5e74faa7bc61af9cda0 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 12:31:29 +0100 Subject: [PATCH 14/21] comment + cleanup --- editor/src/components/canvas/canvas-types.ts | 4 ++-- editor/src/core/tailwind/tailwind-parsing-utils.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 995fef5abe7a..28d7c35b6244 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -24,11 +24,11 @@ import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types import type { MouseButtonsPressed } from '../../utils/mouse' import type { FlexWrap } from 'utopia-api/core' import type { + CSSBorderRadius, CSSNumber, + CSSOverflow, CSSPadding, FlexDirection, - CSSBorderRadius, - CSSOverflow, } from '../inspector/common/css-utils' export const CanvasContainerID = 'canvas-container' diff --git a/editor/src/core/tailwind/tailwind-parsing-utils.ts b/editor/src/core/tailwind/tailwind-parsing-utils.ts index d86d37a46ed4..9e745026121d 100644 --- a/editor/src/core/tailwind/tailwind-parsing-utils.ts +++ b/editor/src/core/tailwind/tailwind-parsing-utils.ts @@ -1,6 +1,7 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' import type { Config } from 'tailwindcss' +// taken from `@xengine/tailwindcss-class-parser` type ParsedTailwindClassVariant = { value: string } & Record type ParsedTailwindValueDef = { value: string; class: Array } & Record From 5db012ce76a43587f0f534f2405263280c91d0b8 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 12:31:46 +0100 Subject: [PATCH 15/21] that was another cursor whoopsie --- editor/src/components/canvas/plugins/tailwind-style-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index e51cffc9c8c9..795c83bf93d2 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -32,7 +32,7 @@ function parseTailwindProperty

( const value = prop === 'padding' && typeof property.value === 'string' ? underscoresToSpaces(property.value) - : mapping[prop] + : property.value const parsed = cssParsers[prop](value, null) if (isLeft(parsed) || parsed.value == null) { return null From d261d57e6801abf0007e9d59850dcf6295731b02 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 14:23:20 +0100 Subject: [PATCH 16/21] create styleinfo even without a style prop to read from --- .../src/components/canvas/plugins/inline-style-plugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 71f3d65b08fd..3bdbded90b87 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -65,8 +65,11 @@ function getUntypedStyleInfo(jsxElementProps: JSXAttributes): UntypedStyleInfo | function getPropertyFromInstance

( prop: P, - untypedStyleInfo: UntypedStyleInfo, + untypedStyleInfo: UntypedStyleInfo | null, ): CSSStyleProperty> | null { + if (untypedStyleInfo == null) { + return cssStylePropertyNotFound() + } const attribute = untypedStyleInfo[prop] if (attribute == null) { return cssStylePropertyNotFound() @@ -109,9 +112,6 @@ export const InlineStylePlugin: StylePlugin = { ({ projectContents }) => (elementPath) => { const untypedStyleInfo = InlineStylePlugin.readUntypedStyleInfo(projectContents, elementPath) - if (untypedStyleInfo == null) { - return null - } const gap = getPropertyFromInstance('gap', untypedStyleInfo) const flexDirection = getPropertyFromInstance('flexDirection', untypedStyleInfo) From 05725d2b149997cff3f8e15375739ecb8437fc5e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 16:12:57 +0100 Subject: [PATCH 17/21] tests: inline -> tailwind --- .../canvas-context-menu.spec.browser2.tsx | 97 +++++++++++++++++-- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx index 6761cad5edd5..34872e882df1 100644 --- a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx @@ -980,6 +980,79 @@ describe('canvas context menu', () => { describe('Inline style <> Tailwind conversion', () => { setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) + + const supportedElementStyles: Array<[keyof React.CSSProperties, string | number, string]> = [ + // TODO: not supported by tailwind-class-parser + // ['objectFit', 'contain', 'object-contain'], + // ['justifyItems', 'center', '], + // ['mixBlendMode', 'multiply', ''], + // ['textDecorationStyle', 'solid', ''], + // ['textShadow', '0 0 #0000', ''], + // ['transform', 'rotate(0deg)', 'rotate-0'], + // ['transformOrigin', 'center', ''], + // ['alignContent', 'center', ''], + // ['gridAutoFlow', 'row', ''], + // ['alignSelf', 'center', ''], + // ['flexBasis', 'auto', ''], + + // TODO: tailwind-class-parser cannot turn this into class names + // ['flexGrow', 0, 'flex-grow-0'], + // ['flexShrink', 0, 'flex-shrink-0'], + + ['backgroundColor', 'red', ''], + ['borderRadius', '4px', 'rounded-[4px]'], + ['borderTopLeftRadius', '5rem', 'rounded-tl-[5rem]'], + ['borderTopRightRadius', '1vh', 'rounded-tr-[1vh]'], + ['borderBottomLeftRadius', '10%', 'rounded-bl-[10%]'], + ['borderBottomRightRadius', '2px', 'rounded-br-[2px]'], + ['boxShadow', '0 0 #0000', 'shadow-[0_0_#0000]'], + ['color', '#334455', 'text-[#334455]'], + ['fontFamily', 'mono', 'font-mono'], + ['fontSize', '30px', 'text-[30px]'], + ['fontStyle', 'italic', 'italic'], + ['fontWeight', 'bold', 'font-bold'], + ['letterSpacing', '0em', 'tracking-normal'], + ['lineHeight', '1rem', 'leading-4'], + ['opacity', '0.4', 'opacity-40'], + ['overflow', 'hidden', 'overflow-hidden'], + ['textAlign', 'center', 'text-center'], + ['textDecorationColor', '#334455', 'decoration-[#334455]'], + ['textDecorationLine', 'underline', 'underline'], + + ['flexWrap', 'wrap', 'flex-wrap'], + ['flexDirection', 'row', 'flex-row'], + ['alignItems', 'center', 'items-center'], + ['justifyContent', 'center', 'justify-center'], + ['padding', '0.5rem', 'p-2'], + ['paddingTop', '0.5rem', 'pt-2'], + ['paddingRight', '0.5rem', 'pr-2'], + ['paddingBottom', '0.5rem', 'pb-2'], + ['paddingLeft', '0.5rem', 'pl-2'], + + ['position', 'absolute', 'absolute'], + ['left', '0.5rem', 'left-2'], + ['top', '0.5rem', 'top-2'], + ['right', '0.5rem', 'right-2'], + ['bottom', '0.5rem', 'bottom-2'], + ['width', '100%', 'w-full'], + ['height', '100%', 'h-full'], + ['minWidth', '100%', 'min-w-full'], + ['maxWidth', 'none', 'max-w-none'], + ['minHeight', '100%', 'min-h-full'], + ['maxHeight', 'none', 'max-h-none'], + ['margin', '20px', 'm-[20px]'], + ['marginTop', 20, 'mt-[20px]'], + ['marginRight', '2rem', 'mr-8'], + ['marginBottom', '20%', 'mb-[20%]'], + ['marginLeft', '0px', 'ml-0'], + ['flex', '0 0 auto', 'flex-[0_0_auto]'], + ['display', 'block', 'block'], + ['gap', '0', 'gap-0'], + ['zIndex', 0, 'z-[0px]'], + ['rowGap', 0, 'gap-x-0'], + ['columnGap', 0, 'gap-y-0'], + ] + it('can convert element styles from Tailwind to inline style', async () => { const editor = await renderTestEditorWithModel( createModifiedProject({ @@ -1051,6 +1124,19 @@ export var storyboard = (props) => { }) it('can convert element styles from inline style to Tailwind', async () => { + const styleProp = supportedElementStyles.reduce( + (acc: Record, [prop, value]) => { + acc[prop] = value + return acc + }, + {}, + ) + + const expectedTailwindClasses = supportedElementStyles + .map(([_, __, tailwindClass]) => tailwindClass) + .filter((className) => className.length > 0) + .join(' ') + const editor = await renderTestEditorWithModel( createModifiedProject({ [StoryboardFilePath]: `import * as React from 'react' @@ -1067,14 +1153,7 @@ export var storyboard = (props) => {

@@ -1112,7 +1191,7 @@ export var storyboard = (props) => { data-testid='bbb' data-uid='bbb' style={{}} - className='absolute bottom-1 left-1 top-1 right-1 bg-red-100' + className='${expectedTailwindClasses}' /> From e7791f5a46d3766d1433fe9aaf47b0658963a870 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 16:28:17 +0100 Subject: [PATCH 18/21] tailwind classname mapping --- .../canvas/plugins/tailwind-style-plugin.ts | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 795c83bf93d2..738879f93ce9 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -14,6 +14,7 @@ import * as PP from '../../../core/shared/property-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' import { getParsedClassList } from '../../../core/tailwind/tailwind-class-list-utils' import { isLeft } from '../../../core/shared/either' +import { textDecorationLine } from '../../../uuiui' const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') @@ -49,6 +50,12 @@ const TailwindPropertyMapping: Record = { width: 'width', height: 'height', + minWidth: 'minWidth', + maxWidth: 'maxWidth', + minHeight: 'minHeight', + maxHeight: 'maxHeight', + + display: 'display', padding: 'padding', paddingTop: 'paddingTop', @@ -56,6 +63,12 @@ const TailwindPropertyMapping: Record = { paddingBottom: 'paddingBottom', paddingLeft: 'paddingLeft', + margin: 'margin', + marginTop: 'marginTop', + marginRight: 'marginRight', + marginBottom: 'marginBottom', + marginLeft: 'marginLeft', + borderRadius: 'borderRadius', borderTopLeftRadius: 'borderTopLeftRadius', borderTopRightRadius: 'borderTopRightRadius', @@ -64,6 +77,9 @@ const TailwindPropertyMapping: Record = { justifyContent: 'justifyContent', alignItems: 'alignItems', + alignSelf: 'alignSelf', + justifySelf: 'justifySelf', + justifyItems: 'justifyItems', flex: 'flex', flexDirection: 'flexDirection', flexGrow: 'flexGrow', @@ -74,9 +90,29 @@ const TailwindPropertyMapping: Record = { overflow: 'overflow', + opacity: 'opacity', + zIndex: 'zIndex', backgroundColor: 'backgroundColor', + + fontSize: 'fontSize', + fontStyle: 'fontStyle', + fontFamily: 'fontFamily', + fontWeight: 'fontWeight', + letterSpacing: 'letterSpacing', + lineHeight: 'lineHeight', + color: 'textColor', + textDecoration: 'textDecoration', + textDecorationColor: 'textDecorationColor', + textDecorationStyle: 'textDecorationStyle', + textDecorationLine: 'textDecoration', + textAlign: 'textAlign', + + rowGap: 'gapX', + columnGap: 'gapY', + + boxShadow: 'boxShadow', } function toCamelCase(str: string): string { @@ -200,16 +236,14 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ updates, ) - const propsToSet = mapDropNulls( - (update) => - update.type !== 'set' || TailwindPropertyMapping[update.property] == null - ? null - : UCL.add({ - property: TailwindPropertyMapping[update.property], - value: stringifyPropertyValue(update.value), - }), - updates, - ) + const propsToSet = mapDropNulls((update) => { + return update.type !== 'set' || TailwindPropertyMapping[update.property] == null + ? null + : UCL.add({ + property: TailwindPropertyMapping[update.property], + value: stringifyPropertyValue(update.value), + }) + }, updates) return UCL.runUpdateClassList( editorState, elementPath, From dcd99fb2cc77cb1005413b6574573d7ba8144150 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 17:16:07 +0100 Subject: [PATCH 19/21] tests: tailwind -> inline style --- .../canvas-context-menu.spec.browser2.tsx | 80 ++++++++----------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx index 34872e882df1..12ed6b261f72 100644 --- a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx @@ -999,24 +999,24 @@ describe('canvas context menu', () => { // ['flexGrow', 0, 'flex-grow-0'], // ['flexShrink', 0, 'flex-shrink-0'], - ['backgroundColor', 'red', ''], + ['backgroundColor', '#334455', 'bg-[#334455]'], ['borderRadius', '4px', 'rounded-[4px]'], - ['borderTopLeftRadius', '5rem', 'rounded-tl-[5rem]'], - ['borderTopRightRadius', '1vh', 'rounded-tr-[1vh]'], - ['borderBottomLeftRadius', '10%', 'rounded-bl-[10%]'], - ['borderBottomRightRadius', '2px', 'rounded-br-[2px]'], + ['borderTopLeftRadius', '4px', 'rounded-tl-[4px]'], + ['borderTopRightRadius', '4px', 'rounded-tr-[4px]'], + ['borderBottomLeftRadius', '4px', 'rounded-bl-[4px]'], + ['borderBottomRightRadius', '4px', 'rounded-br-[4px]'], ['boxShadow', '0 0 #0000', 'shadow-[0_0_#0000]'], - ['color', '#334455', 'text-[#334455]'], + ['color', 'rgb(51, 68, 85)', 'text-[#334455]'], ['fontFamily', 'mono', 'font-mono'], ['fontSize', '30px', 'text-[30px]'], ['fontStyle', 'italic', 'italic'], - ['fontWeight', 'bold', 'font-bold'], + ['fontWeight', '700', 'font-bold'], ['letterSpacing', '0em', 'tracking-normal'], ['lineHeight', '1rem', 'leading-4'], ['opacity', '0.4', 'opacity-40'], ['overflow', 'hidden', 'overflow-hidden'], ['textAlign', 'center', 'text-center'], - ['textDecorationColor', '#334455', 'decoration-[#334455]'], + ['textDecorationColor', 'rgb(51, 68, 85)', 'decoration-[#334455]'], ['textDecorationLine', 'underline', 'underline'], ['flexWrap', 'wrap', 'flex-wrap'], @@ -1041,19 +1041,31 @@ describe('canvas context menu', () => { ['minHeight', '100%', 'min-h-full'], ['maxHeight', 'none', 'max-h-none'], ['margin', '20px', 'm-[20px]'], - ['marginTop', 20, 'mt-[20px]'], - ['marginRight', '2rem', 'mr-8'], - ['marginBottom', '20%', 'mb-[20%]'], - ['marginLeft', '0px', 'ml-0'], + ['marginTop', '20px', 'mt-[20px]'], + ['marginRight', '20px', 'mr-[20px]'], + ['marginBottom', '20px', 'mb-[20px]'], + ['marginLeft', '20px', 'ml-[20px]'], ['flex', '0 0 auto', 'flex-[0_0_auto]'], ['display', 'block', 'block'], - ['gap', '0', 'gap-0'], - ['zIndex', 0, 'z-[0px]'], - ['rowGap', 0, 'gap-x-0'], - ['columnGap', 0, 'gap-y-0'], + ['gap', '0px', 'gap-0'], + ['zIndex', '0', 'z-0'], + ['rowGap', '0px', 'gap-x-0'], + ['columnGap', '0px', 'gap-y-0'], ] it('can convert element styles from Tailwind to inline style', async () => { + const tailwindStyles = supportedElementStyles + .map(([_, __, tailwindClass]) => tailwindClass) + .filter((className) => className.length > 0) + .join(' ') + + const inlineStyles = supportedElementStyles.reduce( + (acc: Record, [prop, value]) => { + acc[prop] = value + return acc + }, + {}, + ) const editor = await renderTestEditorWithModel( createModifiedProject({ [StoryboardFilePath]: `import * as React from 'react' @@ -1069,7 +1081,7 @@ export var storyboard = (props) => {
@@ -1092,35 +1104,13 @@ export var storyboard = (props) => { contextMenuItemLabel: ConvertTailwindToInlineStyleOptionText, }) - expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(`import * as React from 'react' -import { Scene, Storyboard } from 'utopia-api' + inlineStyles['backgroundColor'] = 'rgb(51, 68, 85)' + inlineStyles['boxShadow'] = 'rgba(0, 0, 0, 0) 0px 0px' + inlineStyles['fontFamily'] = + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' -export var storyboard = (props) => { - return ( - - -
- - - ) -} -`) + const elementStyles = editor.renderedDOM.getByTestId('bbb').style + expect(elementStyles).toMatchObject(inlineStyles) }) it('can convert element styles from inline style to Tailwind', async () => { From 92103452631441e154a7eb436a745ba7de602cfe Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 18:23:20 +0100 Subject: [PATCH 20/21] test: non-tailwind props --- .../canvas-context-menu.spec.browser2.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx index 12ed6b261f72..5e28d0561620 100644 --- a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx @@ -1189,6 +1189,56 @@ export var storyboard = (props) => { } `) }) + + it('does not affect non-tailwind classes when converting to inline style', async () => { + const editor = await renderTestEditorWithModel( + createModifiedProject({ + [StoryboardFilePath]: `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = (props) => { + return ( + + +
+ + + ) +}`, + [TailwindConfigPath]: ` + const TailwindConfig = { } + export default TailwindConfig + `, + 'app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities;`, + }), + 'await-first-dom-report', + ) + + await openContextMenuOnElement(editor, { + testId: 'bbb', + contextMenuItemLabel: ConvertTailwindToInlineStyleOptionText, + }) + + const element = editor.renderedDOM.getByTestId('bbb') + const { width, height, borderRadius } = element.style + expect({ width, height, borderRadius }).toEqual({ + width: '1.25rem', + height: '0.5rem', + borderRadius: '0.375rem', + }) + const className = element.className + expect(className).toEqual('btn-big shadow-shiny') + }) }) }) From d53c23ee5529846e43fb3134399842f27f116ac9 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 26 Nov 2024 18:41:38 +0100 Subject: [PATCH 21/21] test: calculated inline style props --- .../canvas-context-menu.spec.browser2.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx index 5e28d0561620..4dfb388db252 100644 --- a/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-context-menu.spec.browser2.tsx @@ -1239,6 +1239,77 @@ export var storyboard = (props) => { const className = element.className expect(className).toEqual('btn-big shadow-shiny') }) + + it('does not affect calculated styles when converting to tailwind', async () => { + const editor = await renderTestEditorWithModel( + createModifiedProject({ + [StoryboardFilePath]: `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +const height = 100 +export var storyboard = (props) => { + return ( + + +
+ + + ) +}`, + [TailwindConfigPath]: ` + const TailwindConfig = { } + export default TailwindConfig + `, + 'app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities;`, + }), + 'await-first-dom-report', + ) + + await openContextMenuOnElement(editor, { + testId: 'bbb', + contextMenuItemLabel: ConvertInlineStyleToTailwindOptionText, + }) + + const element = editor.renderedDOM.getByTestId('bbb') + expect(element.className).toEqual('p-1') + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(`import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +const height = 100 +export var storyboard = (props) => { + return ( + + +
+ + + ) +} +`) + }) }) })