From 9753bef284d4f2ec3cf51479228b5294afb2fc02 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 1 Dec 2024 19:56:36 +0200 Subject: [PATCH 01/21] convert media queries to model --- editor/src/components/canvas/canvas-types.ts | 19 +++- .../canvas/canvas-utils-unit-tests.spec.tsx | 29 ++++- editor/src/components/canvas/canvas-utils.ts | 103 ++++++++++++++++++ 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 06f6f0f5220e..a1181bfb7adf 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -552,11 +552,21 @@ interface CSSStylePropertyNotParsable { interface ParsedCSSStyleProperty { type: 'property' - tags: PropertyTag[] + modifiers: StyleModifier[] propertyValue: JSExpression | PartOfJSXAttributeValue value: T } +type StyleModifier = HoverModifier | MediaSizeModifier +type HoverModifier = { type: 'hover' } +type MediaSizeModifier = { type: 'media-size'; size: ScreenSize } + +// @media (min-width: 100px) and (max-width: 200em) => { min: { value: 100, unit: 'px' }, max: { value: 200, unit: 'em' } } +export type ScreenSize = { + min?: { value: number; unit: string } + max?: { value: number; unit: string } +} + export type CSSStyleProperty = | CSSStylePropertyNotFound | CSSStylePropertyNotParsable @@ -575,8 +585,13 @@ export function cssStylePropertyNotParsable( export function cssStyleProperty( value: T, propertyValue: JSExpression | PartOfJSXAttributeValue, + modifiers: StyleModifier[], ): ParsedCSSStyleProperty { - return { type: 'property', tags: [], value: value, propertyValue: propertyValue } + return { type: 'property', modifiers: [], value: value, propertyValue: propertyValue } +} + +export function screenSizeModifier(size: ScreenSize): MediaSizeModifier { + return { type: 'media-size', size: size } } export function maybePropertyValue(property: CSSStyleProperty): T | null { diff --git a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx index 120da8d8fb17..9295908a74f1 100644 --- a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx +++ b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx @@ -5,11 +5,13 @@ import { testPrintCodeFromEditorState, getEditorState, } from './ui-jsx.test-utils' -import type { EdgePosition } from './canvas-types' +import type { EdgePosition, ScreenSize } from './canvas-types' import { singleResizeChange, pinMoveChange, pinFrameChange } from './canvas-types' import type { CanvasVector } from '../../core/shared/math-utils' import { canvasRectangle } from '../../core/shared/math-utils' -import { updateFramesOfScenesAndComponents } from './canvas-utils' +import type { MediaQuery } from './canvas-utils' +import { mediaQueryToScreenSize, updateFramesOfScenesAndComponents } from './canvas-utils' +import * as csstree from 'css-tree' import { NO_OP } from '../../core/shared/utils' import { editorModelFromPersistentModel } from '../editor/store/editor-state' import { complexDefaultProjectPreParsed } from '../../sample-projects/sample-project-utils.test-utils' @@ -489,3 +491,26 @@ describe('updateFramesOfScenesAndComponents - pinFrameChange -', () => { ) }) }) + +describe('mediaQueryToScreenSize', () => { + it('converts simple screen size queries', () => { + const testCases: { input: string; expected: ScreenSize }[] = [ + { + input: '@media (100px { + csstree.walk(csstree.parse(testCase.input), (node) => { + if (node.type === 'MediaQuery') { + const result = mediaQueryToScreenSize(node as unknown as MediaQuery) + expect(result).toEqual(testCase.expected) + } + }) + }) + }) +}) diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 2bfd96433fdc..69d4a6a699b7 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -1,3 +1,4 @@ +import type * as csstree from 'css-tree' import type { DataRouteObject } from 'react-router' import type { LayoutPinnedProp, LayoutTargetableProp } from '../../core/layout/layout-helpers-new' import { @@ -128,6 +129,7 @@ import type { DuplicateNewUID, EdgePosition, PinOrFlexFrameChange, + ScreenSize, } from './canvas-types' import { flexResizeChange, pinFrameChange } from './canvas-types' import { @@ -2260,3 +2262,104 @@ export function projectContentsSameForRefreshRequire( // If nothing differs, return true. return true } + +export interface MediaQuery { + type: 'MediaQuery' + loc: null + modifier: null + mediaType: null + condition?: { + type: 'Condition' + loc: null + kind: 'media' + children: Array + } +} + +interface FeatureRange { + type: 'FeatureRange' + loc: null + kind: 'media' + left?: csstree.Dimension + leftComparison: '<' | '>' + middle: csstree.Identifier + rightComparison: '<' | '>' + right?: csstree.Dimension +} + +interface Feature { + type: 'Feature' + loc: null + kind: 'media' + name: 'min-width' | 'max-width' + value?: csstree.Dimension +} + +export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { + const result: ScreenSize = {} + + if (mediaQuery.condition?.type === 'Condition') { + // Handle FeatureRange case + const featureRange = mediaQuery.condition.children.find( + (child): child is FeatureRange => child.type === 'FeatureRange', + ) + + if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { + const leftValue = + featureRange.left?.type === 'Dimension' + ? { + value: Number(featureRange.left.value), + unit: featureRange.left.unit, + } + : null + + const rightValue = + featureRange.right?.type === 'Dimension' + ? { + value: Number(featureRange.right.value), + unit: featureRange.right.unit, + } + : null + + // Left value determines if it's min (<) or max (>) + if (leftValue != null) { + if (featureRange.leftComparison === '<') { + result.min = leftValue + } else { + result.max = leftValue + } + } + + // Right value determines if it's max (<) or min (>) + if (rightValue != null) { + if (featureRange.rightComparison === '<') { + result.max = rightValue + } else { + result.min = rightValue + } + } + } + + // Handle Feature case (min-width/max-width) + const features = mediaQuery.condition.children.filter( + (child): child is Feature => child.type === 'Feature', + ) + features.forEach((feature) => { + if (feature.value?.type === 'Dimension') { + if (feature.name === 'min-width') { + result.min = { + value: Number(feature.value.value), + unit: feature.value.unit, + } + } else if (feature.name === 'max-width') { + result.max = { + value: Number(feature.value.value), + unit: feature.value.unit, + } + } + } + }) + } + + return result +} From 7502d5f9afc702c937441b7cd6383ed89df35f43 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 2 Dec 2024 11:56:30 +0200 Subject: [PATCH 02/21] fix logic and add more tests --- .../canvas/canvas-utils-unit-tests.spec.tsx | 12 +++ editor/src/components/canvas/canvas-utils.ts | 98 +++++++++++++------ 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx index 9295908a74f1..417540fe6d11 100644 --- a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx +++ b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx @@ -503,6 +503,18 @@ describe('mediaQueryToScreenSize', () => { input: '@media (min-width: 100px) and (max-width: 500px)', expected: { min: { value: 100, unit: 'px' }, max: { value: 500, unit: 'px' } }, }, + { + input: '@media screen and (min-width: 100px)', + expected: { min: { value: 100, unit: 'px' } }, + }, + { + input: '@media (100px < width) and (max-width: 500px)', + expected: { min: { value: 100, unit: 'px' }, max: { value: 500, unit: 'px' } }, + }, + { + input: '@media (width > 100px)', + expected: { min: { value: 100, unit: 'px' } }, + }, ] testCases.forEach((testCase) => { csstree.walk(csstree.parse(testCase.input), (node) => { diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 69d4a6a699b7..7a01891e673e 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -2280,11 +2280,11 @@ interface FeatureRange { type: 'FeatureRange' loc: null kind: 'media' - left?: csstree.Dimension + left?: csstree.Dimension | csstree.Identifier leftComparison: '<' | '>' - middle: csstree.Identifier + middle: csstree.Dimension | csstree.Identifier rightComparison: '<' | '>' - right?: csstree.Dimension + right?: csstree.Dimension | csstree.Identifier } interface Feature { @@ -2295,50 +2295,92 @@ interface Feature { value?: csstree.Dimension } +type CompValue = { + value: number + unit: string +} +function extractFromFeatureRange(featureRange: FeatureRange): { + leftValue: CompValue | null + rightValue: CompValue | null + leftComparison: '<' | '>' | null + rightComparison: '<' | '>' | null +} | null { + // 100px < width < 500px or 500px > width > 100px or 100px > width or 500px < width + if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { + const leftValue = + featureRange.left?.type === 'Dimension' + ? { + value: Number(featureRange.left.value), + unit: featureRange.left.unit, + } + : null + + const rightValue = + featureRange.right?.type === 'Dimension' + ? { + value: Number(featureRange.right.value), + unit: featureRange.right.unit, + } + : null + + return { + leftValue: leftValue, + rightValue: rightValue, + leftComparison: featureRange.leftComparison, + rightComparison: featureRange.rightComparison, + } + } + // width > 100px or width < 500px + if (featureRange?.left?.type === 'Identifier' && featureRange.left.name === 'width') { + const rightValue = + featureRange.middle?.type === 'Dimension' + ? { + value: Number(featureRange.middle.value), + unit: featureRange.middle.unit, + } + : null + // this is not a mistake, since we normalize the "width" to be in the middle + const rightComparison = featureRange.leftComparison + + return { + leftValue: null, + leftComparison: null, + rightValue: rightValue, + rightComparison: rightComparison, + } + } + return null +} export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { const result: ScreenSize = {} if (mediaQuery.condition?.type === 'Condition') { // Handle FeatureRange case - const featureRange = mediaQuery.condition.children.find( + const featureRanges = mediaQuery.condition.children.filter( (child): child is FeatureRange => child.type === 'FeatureRange', - ) - - if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { - const leftValue = - featureRange.left?.type === 'Dimension' - ? { - value: Number(featureRange.left.value), - unit: featureRange.left.unit, - } - : null - - const rightValue = - featureRange.right?.type === 'Dimension' - ? { - value: Number(featureRange.right.value), - unit: featureRange.right.unit, - } - : null + ) as Array - // Left value determines if it's min (<) or max (>) + featureRanges.forEach((featureRange) => { + const rangeData = extractFromFeatureRange(featureRange) + if (rangeData == null) { + return + } + const { leftValue, rightValue, leftComparison, rightComparison } = rangeData if (leftValue != null) { - if (featureRange.leftComparison === '<') { + if (leftComparison === '<') { result.min = leftValue } else { result.max = leftValue } } - - // Right value determines if it's max (<) or min (>) if (rightValue != null) { - if (featureRange.rightComparison === '<') { + if (rightComparison === '<') { result.max = rightValue } else { result.min = rightValue } } - } + }) // Handle Feature case (min-width/max-width) const features = mediaQuery.condition.children.filter( From 881dc22f9df444dc8eaf7e7f222e1c43bbf97640 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 2 Dec 2024 16:42:39 +0200 Subject: [PATCH 03/21] add modifiers wip --- editor/src/components/canvas/canvas-types.ts | 19 +- editor/src/components/canvas/canvas-utils.ts | 5 +- .../canvas/plugins/tailwind-style-plugin.ts | 313 +++++++++++------- 3 files changed, 200 insertions(+), 137 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index a1181bfb7adf..037c382b5677 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -557,14 +557,18 @@ interface ParsedCSSStyleProperty { value: T } -type StyleModifier = HoverModifier | MediaSizeModifier +export type StyleModifier = HoverModifier | MediaSizeModifier type HoverModifier = { type: 'hover' } type MediaSizeModifier = { type: 'media-size'; size: ScreenSize } +export type CompValue = { + value: number + unit: string +} // @media (min-width: 100px) and (max-width: 200em) => { min: { value: 100, unit: 'px' }, max: { value: 200, unit: 'em' } } export type ScreenSize = { - min?: { value: number; unit: string } - max?: { value: number; unit: string } + min?: CompValue + max?: CompValue } export type CSSStyleProperty = @@ -585,9 +589,14 @@ export function cssStylePropertyNotParsable( export function cssStyleProperty( value: T, propertyValue: JSExpression | PartOfJSXAttributeValue, - modifiers: StyleModifier[], + modifiers?: StyleModifier[], ): ParsedCSSStyleProperty { - return { type: 'property', modifiers: [], value: value, propertyValue: propertyValue } + return { + type: 'property', + modifiers: modifiers ?? [], + value: value, + propertyValue: propertyValue, + } } export function screenSizeModifier(size: ScreenSize): MediaSizeModifier { diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 7a01891e673e..a9879e0d658a 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -126,6 +126,7 @@ import * as EP from '../../core/shared/element-path' import * as PP from '../../core/shared/property-path' import type { CanvasFrameAndTarget, + CompValue, DuplicateNewUID, EdgePosition, PinOrFlexFrameChange, @@ -2295,10 +2296,6 @@ interface Feature { value?: csstree.Dimension } -type CompValue = { - value: number - unit: string -} function extractFromFeatureRange(featureRange: FeatureRange): { leftValue: CompValue | null rightValue: CompValue | null diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 2941f0bc9a12..27523f5e0093 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -7,7 +7,7 @@ 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, StyleModifier } 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' @@ -19,15 +19,35 @@ import { emptyComments, type JSXAttributes } from 'utopia-shared/src/types' import * as PP from '../../../core/shared/property-path' import { jsExpressionValue } from '../../../core/shared/element-template' -function parseTailwindProperty( - value: string | number | undefined, - prop: T, -): CSSStyleProperty> | null { - const parsed = cssParsers[prop](value, null) - if (isLeft(parsed) || parsed.value == null) { - return null +const parseTailwindPropertyWithConfig = + (config: Config | null) => + ( + styleDefinition: TailwindStyleDefinition | undefined, + prop: T, + ): CSSStyleProperty> | null => { + if (styleDefinition == null) { + return null + } + const parsed = cssParsers[prop](styleDefinition.value, null) + if (isLeft(parsed) || parsed.value == null) { + return null + } + const modifiers = getModifiers(styleDefinition.variants ?? [], config) + return cssStyleProperty( + parsed.value, + jsExpressionValue(styleDefinition.value, emptyComments), + modifiers, + ) } - return cssStyleProperty(parsed.value, jsExpressionValue(value, emptyComments)) + +function getModifiers( + variants: { type: string; value: string }[], + config: Config | null, +): StyleModifier[] { + // media modifiers + const mediaModifiers = variants.filter((v) => v.type === 'media') + const screenSizes = config?.theme?.screens ?? {} + return [] } const TailwindPropertyMapping: Record = { @@ -81,136 +101,173 @@ function stringifyPropertyValue(value: string | number): string { } } -function getTailwindClassMapping(classes: string[], config: Config | null): Record { - const mapping: Record = {} +type TailwindStyleDefinition = { + value: string | number | undefined + variants?: { type: string; value: string }[] +} +type TailwindParsedStyle = { + kind: string + property: string + value: string + variants?: { type: string; value: string }[] +} +function getTailwindClassMapping( + classes: string[], + config: Config | null, +): Record { + const mapping: Record = {} classes.forEach((className) => { - const parsed = TailwindClassParser.parse(className, config ?? undefined) - if (parsed.kind === 'error' || !isSupportedTailwindProperty(parsed.property)) { + const parsed: TailwindParsedStyle | undefined = TailwindClassParser.parse( + className, + config ?? undefined, + ) + if ( + parsed == null || + parsed.kind === 'error' || + !isSupportedTailwindProperty(parsed.property) + ) { return } - mapping[parsed.property] = parsed.value + mapping[parsed.property] = { + value: parsed.value, + variants: parsed.variants, + } }) return mapping } -const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') - -export const TailwindPlugin = (config: Config | null): StylePlugin => ({ - name: 'Tailwind', - readStyleFromElementProps:

( - attributes: JSXAttributes, - prop: P, - ): CSSStyleProperty> | null => { - const classNameAttribute = defaultEither( - null, - flatMapEither( - (attr) => jsxSimpleAttributeToValue(attr), - getModifiableJSXAttributeAtPath(attributes, PP.create('className')), - ), - ) +const underscoresToSpaces = ( + styleDef: TailwindStyleDefinition | undefined, +): TailwindStyleDefinition | undefined => { + if (styleDef == null) { + return undefined + } + return { + ...styleDef, + value: + typeof styleDef.value === 'string' ? styleDef.value.replace(/[-_]/g, ' ') : styleDef.value, + } +} - if (typeof classNameAttribute !== 'string') { - return null - } +export const TailwindPlugin = (config: Config | null): StylePlugin => { + const parseTailwindProperty = parseTailwindPropertyWithConfig(config) + return { + name: 'Tailwind', + readStyleFromElementProps:

( + attributes: JSXAttributes, + prop: P, + ): CSSStyleProperty> | null => { + const classNameAttribute = defaultEither( + null, + flatMapEither( + (attr) => jsxSimpleAttributeToValue(attr), + getModifiableJSXAttributeAtPath(attributes, PP.create('className')), + ), + ) - const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) - return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop) - }, - styleInfoFactory: - ({ projectContents }) => - (elementPath) => { - const classList = getClassNameAttribute( - getElementFromProjectContents(elementPath, projectContents), - )?.value - - if (classList == null || typeof classList !== 'string') { + if (typeof classNameAttribute !== 'string') { return null } - 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', - ), - borderRadius: parseTailwindProperty( - mapping[TailwindPropertyMapping.borderRadius], - 'borderRadius', - ), - borderTopLeftRadius: parseTailwindProperty( - mapping[TailwindPropertyMapping.borderTopLeftRadius], - 'borderTopLeftRadius', - ), - borderTopRightRadius: parseTailwindProperty( - mapping[TailwindPropertyMapping.borderTopRightRadius], - 'borderTopRightRadius', - ), - borderBottomRightRadius: parseTailwindProperty( - mapping[TailwindPropertyMapping.borderBottomRightRadius], - 'borderBottomRightRadius', - ), - borderBottomLeftRadius: parseTailwindProperty( - mapping[TailwindPropertyMapping.borderBottomLeftRadius], - 'borderBottomLeftRadius', - ), - zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'), - flexWrap: parseTailwindProperty(mapping[TailwindPropertyMapping.flexWrap], 'flexWrap'), - overflow: parseTailwindProperty(mapping[TailwindPropertyMapping.overflow], 'overflow'), - } + const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) + return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop) }, - updateStyles: (editorState, elementPath, updates) => { - const propsToDelete = mapDropNulls( - (update) => - update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe - ? null - : UCL.remove(TailwindPropertyMapping[update.property]), - updates, - ) + styleInfoFactory: + ({ projectContents }) => + (elementPath) => { + const classList = getClassNameAttribute( + getElementFromProjectContents(elementPath, projectContents), + )?.value - const propsToSet = mapDropNulls( - (update) => - update.type !== 'set' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe - ? null - : UCL.add({ - property: TailwindPropertyMapping[update.property], - value: stringifyPropertyValue(update.value), - }), - updates, - ) - return UCL.runUpdateClassList( - editorState, - elementPath, - [...propsToDelete, ...propsToSet], - config, - ) - }, -}) + if (classList == null || typeof classList !== 'string') { + return null + } + + 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', + ), + borderRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderRadius], + 'borderRadius', + ), + borderTopLeftRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderTopLeftRadius], + 'borderTopLeftRadius', + ), + borderTopRightRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderTopRightRadius], + 'borderTopRightRadius', + ), + borderBottomRightRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderBottomRightRadius], + 'borderBottomRightRadius', + ), + borderBottomLeftRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderBottomLeftRadius], + 'borderBottomLeftRadius', + ), + zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'), + flexWrap: parseTailwindProperty(mapping[TailwindPropertyMapping.flexWrap], 'flexWrap'), + overflow: parseTailwindProperty(mapping[TailwindPropertyMapping.overflow], 'overflow'), + } + }, + updateStyles: (editorState, elementPath, updates) => { + const propsToDelete = mapDropNulls( + (update) => + update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe + ? null + : UCL.remove(TailwindPropertyMapping[update.property]), + updates, + ) + + const propsToSet = mapDropNulls( + (update) => + update.type !== 'set' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe + ? null + : UCL.add({ + property: TailwindPropertyMapping[update.property], + value: stringifyPropertyValue(update.value), + }), + updates, + ) + return UCL.runUpdateClassList( + editorState, + elementPath, + [...propsToDelete, ...propsToSet], + config, + ) + }, + } +} From f2a9d6ef2bcc749a49599011a2c397e6749b0699 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 2 Dec 2024 21:54:10 +0200 Subject: [PATCH 04/21] send correct info --- editor/src/components/canvas/canvas-types.ts | 23 ++- editor/src/components/canvas/canvas-utils.ts | 12 +- .../canvas/plugins/tailwind-style-plugin.ts | 156 ++++++++++++++---- 3 files changed, 155 insertions(+), 36 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 037c382b5677..62f92d512663 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -552,14 +552,18 @@ interface CSSStylePropertyNotParsable { interface ParsedCSSStyleProperty { type: 'property' - modifiers: StyleModifier[] propertyValue: JSExpression | PartOfJSXAttributeValue value: T + appliedModifiers?: StyleModifier[] + variants?: { + value: T + modifiers?: StyleModifier[] + }[] } -export type StyleModifier = HoverModifier | MediaSizeModifier -type HoverModifier = { type: 'hover' } -type MediaSizeModifier = { type: 'media-size'; size: ScreenSize } +type StyleHoverModifier = { type: 'hover' } +export type StyleMediaSizeModifier = { type: 'media-size'; size: ScreenSize } +export type StyleModifier = StyleHoverModifier | StyleMediaSizeModifier export type CompValue = { value: number unit: string @@ -589,17 +593,22 @@ export function cssStylePropertyNotParsable( export function cssStyleProperty( value: T, propertyValue: JSExpression | PartOfJSXAttributeValue, - modifiers?: StyleModifier[], + appliedModifiers?: StyleModifier[], + variants?: { + value: T + modifiers?: StyleModifier[] + }[], ): ParsedCSSStyleProperty { return { type: 'property', - modifiers: modifiers ?? [], + variants: variants ?? [], value: value, propertyValue: propertyValue, + appliedModifiers: appliedModifiers, } } -export function screenSizeModifier(size: ScreenSize): MediaSizeModifier { +export function screenSizeModifier(size: ScreenSize): StyleMediaSizeModifier { return { type: 'media-size', size: size } } diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index a9879e0d658a..487dde4672b6 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -1,4 +1,4 @@ -import type * as csstree from 'css-tree' +import * as csstree from 'css-tree' import type { DataRouteObject } from 'react-router' import type { LayoutPinnedProp, LayoutTargetableProp } from '../../core/layout/layout-helpers-new' import { @@ -2402,3 +2402,13 @@ export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { return result } + +export function extractMediaQueryFromCss(css: string): MediaQuery | null { + let result: MediaQuery | null = null + csstree.walk(csstree.parse(css), (node) => { + if (node.type === 'MediaQuery') { + result = node as unknown as MediaQuery + } + }) + return result +} diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 27523f5e0093..2337f4417ca3 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -1,5 +1,6 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' -import { defaultEither, flatMapEither, isLeft } from '../../../core/shared/either' +import type { Right } from '../../../core/shared/either' +import { defaultEither, Either, flatMapEither, isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { getElementFromProjectContents } from '../../editor/store/editor-state' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' @@ -7,7 +8,7 @@ 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, StyleModifier } from '../canvas-types' +import type { StyleInfo, StyleMediaSizeModifier, StyleModifier } 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' @@ -18,36 +19,137 @@ import { 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 { extractMediaQueryFromCss, mediaQueryToScreenSize } from '../canvas-utils' + +type StyleValueVariants = { + value: string | number | undefined + modifiers?: TailwindGeneralModifier[] +}[] +export type TailwindMediaModifier = { type: 'media'; value: string } +export type TailwindHoverModifier = { type: 'hover'; value: string } +export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier +type ParsedTailwindVariant = { + parsedValue: NonNullable + originalValue: string | number | undefined + modifiers?: StyleModifier[] +} const parseTailwindPropertyWithConfig = (config: Config | null) => ( - styleDefinition: TailwindStyleDefinition | undefined, + styleDefinition: StyleValueVariants | undefined, prop: T, ): CSSStyleProperty> | null => { if (styleDefinition == null) { return null } - const parsed = cssParsers[prop](styleDefinition.value, null) - if (isLeft(parsed) || parsed.value == null) { + const parsed: ParsedTailwindVariant[] = styleDefinition + .map((v) => ({ + parsedValue: cssParsers[prop](v.value, null), + originalValue: v.value, + modifiers: getModifiers(v.modifiers ?? [], config), + })) + .filter((v) => v.parsedValue != null && !isLeft(v.parsedValue)) + .map((v) => ({ + ...v, + parsedValue: (v.parsedValue as Right).value as NonNullable< + ParsedCSSProperties[T] + >, + })) + + const result = selectValueByBreakpoint(parsed) + if (result == null) { return null } - const modifiers = getModifiers(styleDefinition.variants ?? [], config) return cssStyleProperty( - parsed.value, - jsExpressionValue(styleDefinition.value, emptyComments), - modifiers, + result.parsedValue, + jsExpressionValue(result.originalValue, emptyComments), + result.modifiers, + parsed.map((variant) => ({ + value: variant.parsedValue, + modifiers: variant.modifiers, + })), ) } +function selectValueByBreakpoint( + parsedVariants: ParsedTailwindVariant[], +): { + parsedValue: NonNullable + originalValue: string | number | undefined + modifiers?: StyleModifier[] +} | null { + let chosen = parsedVariants[0] ?? null + for (const variant of parsedVariants) { + if (variant.modifiers?.length === 0) { + chosen = variant + break + } + } + if (chosen == null || chosen.parsedValue == null) { + return null + } + return { + parsedValue: chosen.parsedValue, + originalValue: chosen.originalValue, + modifiers: chosen.modifiers, + } +} + +type TailwindScreen = string | { min: string; max: string } + +/** + * This function gets variants in the form of {type: 'media', value: 'sm'} + * and turns them into modifiers in the form of [{type: 'media-size', size: {min: {value: 0, unit: 'px'}, max: {value: 100, unit: 'em'}}}] + * according to the tailwind config + */ function getModifiers( variants: { type: string; value: string }[], config: Config | null, ): StyleModifier[] { // media modifiers const mediaModifiers = variants.filter((v) => v.type === 'media') - const screenSizes = config?.theme?.screens ?? {} - return [] + const screenSizes: Record = (config?.theme?.screens as Record< + string, + TailwindScreen + >) ?? { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + ...((config?.theme?.extend?.screens as Record) ?? {}), + } + return mediaModifiers + .map((mediaModifier) => { + const size: string | { min: string; max: string } | undefined = + screenSizes[mediaModifier.value as keyof typeof screenSizes] + if (size == null) { + return null + } + const mediaString = + typeof size === 'string' + ? `@media (min-width: ${size})` + : `@media ${[ + size.min != null ? `(min-width: ${size.min})` : '', + size.max != null ? `(max-width: ${size.max})` : '', + ] + .filter((s) => s != '') + .join(' and ')}` + const parsed = extractMediaQueryFromCss(mediaString) + if (parsed == null) { + return null + } + const asScreenSize = mediaQueryToScreenSize(parsed) + if (asScreenSize == null) { + return null + } + return { + type: 'media-size', + size: asScreenSize, + } + }) + .filter((m): m is StyleMediaSizeModifier => m != null) } const TailwindPropertyMapping: Record = { @@ -100,11 +202,6 @@ function stringifyPropertyValue(value: string | number): string { assertNever(value) } } - -type TailwindStyleDefinition = { - value: string | number | undefined - variants?: { type: string; value: string }[] -} type TailwindParsedStyle = { kind: string property: string @@ -114,8 +211,8 @@ type TailwindParsedStyle = { function getTailwindClassMapping( classes: string[], config: Config | null, -): Record { - const mapping: Record = {} +): Record { + const mapping: Record = {} classes.forEach((className) => { const parsed: TailwindParsedStyle | undefined = TailwindClassParser.parse( className, @@ -128,25 +225,28 @@ function getTailwindClassMapping( ) { return } - mapping[parsed.property] = { + mapping[parsed.property] = mapping[parsed.property] ?? [] + const modifiers = (parsed.variants ?? []).filter( + (v): v is TailwindGeneralModifier => v.type === 'media' || v.type === 'hover', + ) + mapping[parsed.property].push({ value: parsed.value, - variants: parsed.variants, - } + modifiers: modifiers, + }) }) return mapping } const underscoresToSpaces = ( - styleDef: TailwindStyleDefinition | undefined, -): TailwindStyleDefinition | undefined => { + styleDef: StyleValueVariants | undefined, +): StyleValueVariants | undefined => { if (styleDef == null) { return undefined } - return { - ...styleDef, - value: - typeof styleDef.value === 'string' ? styleDef.value.replace(/[-_]/g, ' ') : styleDef.value, - } + return styleDef.map((style) => ({ + ...style, + value: typeof style.value === 'string' ? style.value.replace(/[-_]/g, ' ') : style.value, + })) } export const TailwindPlugin = (config: Config | null): StylePlugin => { From b4a1f4cd7513e6933f410449c909218e2e5058cf Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 2 Dec 2024 23:17:10 +0200 Subject: [PATCH 05/21] choose breakpoint --- editor/src/components/canvas/canvas-types.ts | 8 ++ .../canvas/plugins/tailwind-style-plugin.ts | 47 +++------- .../inspector/common/css-utils.spec.ts | 58 ++++++++++++ .../components/inspector/common/css-utils.ts | 88 +++++++++++++++++++ 4 files changed, 168 insertions(+), 33 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 62f92d512663..9ae5a483ed57 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -29,6 +29,7 @@ import type { CSSOverflow, CSSPadding, FlexDirection, + ParsedCSSProperties, } from '../inspector/common/css-utils' export const CanvasContainerID = 'canvas-container' @@ -564,6 +565,13 @@ interface ParsedCSSStyleProperty { type StyleHoverModifier = { type: 'hover' } export type StyleMediaSizeModifier = { type: 'media-size'; size: ScreenSize } export type StyleModifier = StyleHoverModifier | StyleMediaSizeModifier + +export type ParsedVariant = { + parsedValue: NonNullable + originalValue: string | number | undefined + modifiers?: StyleModifier[] +} + export type CompValue = { value: number unit: string diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 2337f4417ca3..35bc5793ca71 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -4,11 +4,16 @@ import { defaultEither, Either, flatMapEither, isLeft } from '../../../core/shar import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { getElementFromProjectContents } from '../../editor/store/editor-state' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' -import { cssParsers } from '../../inspector/common/css-utils' +import { cssParsers, selectValueByBreakpoint } 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, StyleMediaSizeModifier, StyleModifier } from '../canvas-types' +import type { + ParsedVariant, + StyleInfo, + StyleMediaSizeModifier, + StyleModifier, +} 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' @@ -28,11 +33,6 @@ type StyleValueVariants = { export type TailwindMediaModifier = { type: 'media'; value: string } export type TailwindHoverModifier = { type: 'hover'; value: string } export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier -type ParsedTailwindVariant = { - parsedValue: NonNullable - originalValue: string | number | undefined - modifiers?: StyleModifier[] -} const parseTailwindPropertyWithConfig = (config: Config | null) => @@ -43,7 +43,7 @@ const parseTailwindPropertyWithConfig = if (styleDefinition == null) { return null } - const parsed: ParsedTailwindVariant[] = styleDefinition + const parsed: ParsedVariant[] = styleDefinition .map((v) => ({ parsedValue: cssParsers[prop](v.value, null), originalValue: v.value, @@ -57,7 +57,12 @@ const parseTailwindPropertyWithConfig = >, })) - const result = selectValueByBreakpoint(parsed) + // to be passed somehow + const scene = document.querySelector('[data-testid=remix-scene]') + if (scene == null) { + return null + } + const result = selectValueByBreakpoint(parsed, parseFloat(getComputedStyle(scene).width)) if (result == null) { return null } @@ -72,30 +77,6 @@ const parseTailwindPropertyWithConfig = ) } -function selectValueByBreakpoint( - parsedVariants: ParsedTailwindVariant[], -): { - parsedValue: NonNullable - originalValue: string | number | undefined - modifiers?: StyleModifier[] -} | null { - let chosen = parsedVariants[0] ?? null - for (const variant of parsedVariants) { - if (variant.modifiers?.length === 0) { - chosen = variant - break - } - } - if (chosen == null || chosen.parsedValue == null) { - return null - } - return { - parsedValue: chosen.parsedValue, - originalValue: chosen.originalValue, - modifiers: chosen.modifiers, - } -} - type TailwindScreen = string | { min: string; max: string } /** diff --git a/editor/src/components/inspector/common/css-utils.spec.ts b/editor/src/components/inspector/common/css-utils.spec.ts index b27f1c8d8303..132561c71912 100644 --- a/editor/src/components/inspector/common/css-utils.spec.ts +++ b/editor/src/components/inspector/common/css-utils.spec.ts @@ -81,10 +81,12 @@ import { printBackgroundSize, printGridDimensionCSS, RegExpLibrary, + selectValueByBreakpoint, stringifyGridDimension, toggleSimple, toggleStylePropPath, } from './css-utils' +import type { StyleMediaSizeModifier } from '../../canvas/canvas-types' describe('toggleStyleProp', () => { const simpleToggleProp = toggleStylePropPath(PP.create('style', 'backgroundColor'), toggleSimple) @@ -2135,3 +2137,59 @@ describe('parseGridRange', () => { expect(got).toEqual(right(gridRange(gridSpanArea('some-area'), gridPositionValue(2)))) }) }) + +describe('selectValueByBreakpoint', () => { + const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ + { + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }, + { + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }, + { + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }, + { value: 'd' }, + ] + it('selects the correct value', () => { + expect(selectValueByBreakpoint(variants, 150)).toEqual({ + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }) + }) + it('select the closest value', () => { + expect(selectValueByBreakpoint(variants, 250)).toEqual({ + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }) + }) + it('converts em to px', () => { + expect(selectValueByBreakpoint(variants, 350)).toEqual({ + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }) + }) + it('selects the first value if no breakpoint is matched', () => { + expect(selectValueByBreakpoint(variants, 50)).toEqual({ value: 'd' }) + }) + it('selects null if no matching breakpoint and no default value', () => { + expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() + }) + it('selects default value if no media modifiers', () => { + expect( + selectValueByBreakpoint( + [ + { + value: 'a', + modifiers: [{ type: 'hover' }], + }, + { value: 'c' }, + ], + 50, + ), + ).toEqual({ value: 'c' }) + }) +}) diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 6147a5ce6441..1fdf9f62637f 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -86,6 +86,7 @@ import { memoize } from '../../../core/shared/memoize' import * as csstree from 'css-tree' import type { IcnProps } from '../../../uuiui' import { cssNumberEqual } from '../../canvas/controls/select-mode/controls-common' +import type { CompValue, StyleMediaSizeModifier, StyleModifier } from '../../canvas/canvas-types' var combineRegExp = function (regexpList: Array, flags?: string) { let source: string = '' @@ -5980,3 +5981,90 @@ export function maybeParseGridLine( return null } } + +const EM_TO_PX_RATIO = 16 +function compValueAsPx(value: CompValue | null | undefined): number | null { + return value == null ? null : value.unit === 'em' ? value.value * EM_TO_PX_RATIO : value.value +} + +function getMediaModifier( + modifiers: StyleModifier[] | undefined | null, +): StyleMediaSizeModifier | null { + return (modifiers ?? []).filter( + (modifier): modifier is StyleMediaSizeModifier => modifier.type === 'media-size', + )[0] +} + +export function selectValueByBreakpoint( + parsedVariants: T[], + sceneWidthInPx: number, +): T | null { + const relevantModifiers = parsedVariants.filter((variant) => { + // 1. filter out variants that don't have media modifiers, but keep variants with no modifiers at all + if (variant.modifiers == null || variant.modifiers.length === 0) { + return true + } + // FIXME - we take only the first media modifier for now + const mediaModifier = getMediaModifier(variant.modifiers) + if (mediaModifier == null) { + // this means it only has other modifiers + return false + } + + // 2. check that it has at least one media modifier that satisfies the current scene width + const maxSizeInPx = compValueAsPx(mediaModifier.size.max) + const minSizeInPx = compValueAsPx(mediaModifier.size.min) + + // if it has only max + if (maxSizeInPx != null && minSizeInPx == null && sceneWidthInPx <= maxSizeInPx) { + return true + } + + // if it has only min + if (maxSizeInPx == null && minSizeInPx != null && sceneWidthInPx >= minSizeInPx) { + return true + } + + // if it has both max and min + if ( + maxSizeInPx != null && + minSizeInPx != null && + sceneWidthInPx >= minSizeInPx && + sceneWidthInPx <= maxSizeInPx + ) { + return true + } + return false + }) + let chosen: T | null = null + for (const variant of relevantModifiers) { + const chosenMediaModifier = getMediaModifier(chosen?.modifiers) + const variantMediaModifier = getMediaModifier(variant.modifiers) + if (variantMediaModifier == null) { + if (chosenMediaModifier == null) { + // if we have nothing chosen then we'll take the base value + chosen = variant + } + continue + } + if (chosenMediaModifier == null) { + chosen = variant + continue + } + // fixme - select by actual media query precedence + const minSizeInPx = compValueAsPx(variantMediaModifier.size.min) + const chosenMinSizeInPx = compValueAsPx(chosenMediaModifier.size.min) + if (minSizeInPx != null && (chosenMinSizeInPx == null || minSizeInPx > chosenMinSizeInPx)) { + chosen = variant + } + const maxSizeInPx = compValueAsPx(variantMediaModifier.size.max) + const chosenMaxSizeInPx = compValueAsPx(chosenMediaModifier.size.max) + if (maxSizeInPx != null && (chosenMaxSizeInPx == null || maxSizeInPx < chosenMaxSizeInPx)) { + chosen = variant + } + } + if (chosen == null) { + return null + } + return chosen +} From e7ed13264c8ca70fa74e50fc75a9b0d3b2a4ec33 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 3 Dec 2024 00:14:34 +0200 Subject: [PATCH 06/21] add buttons --- .../select-mode/remix-scene-label.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx b/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx index 2597448acfe4..158cc437b091 100644 --- a/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx +++ b/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx @@ -324,6 +324,10 @@ const RemixSceneLabel = React.memo((props) => {

{scenelabel}{' '} {sceneSize} + + + +
((props) => { ) }) + +const DeviceSizeButton = React.memo<{ name: string; sizePx: number }>((props) => ( + { + const scene = document.querySelector('[data-testid=remix-scene]') + if (scene != null) { + ;(scene as HTMLElement).style.width = props.sizePx === 0 ? '' : `${props.sizePx}px` + } + e.preventDefault() + e.stopPropagation() + // eslint-disable-next-line + // @ts-ignore + e.stopImmediatePropagation() + }} + onMouseDown={(e) => e.stopPropagation()} + > + {props.name} + +)) From 5207c5350078bb77d7716497fd4c3043a5dda284 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 3 Dec 2024 15:59:50 +0200 Subject: [PATCH 07/21] store tw modifier --- editor/src/components/canvas/canvas-types.ts | 11 +++++++++-- .../canvas/plugins/tailwind-style-plugin.ts | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 9ae5a483ed57..1fb68623ee33 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -562,9 +562,16 @@ interface ParsedCSSStyleProperty { }[] } -type StyleHoverModifier = { type: 'hover' } -export type StyleMediaSizeModifier = { type: 'media-size'; size: ScreenSize } +type StyleModifierMetadata = { type: string; modifierOrigin?: StyleModifierOrigin } +type StyleHoverModifier = StyleModifierMetadata & { type: 'hover' } +export type StyleMediaSizeModifier = StyleModifierMetadata & { + type: 'media-size' + size: ScreenSize +} export type StyleModifier = StyleHoverModifier | StyleMediaSizeModifier +type InlineModifierOrigin = { type: 'inline' } +type TailwindModifierOrigin = { type: 'tailwind'; variant: string } +export type StyleModifierOrigin = InlineModifierOrigin | TailwindModifierOrigin export type ParsedVariant = { parsedValue: NonNullable diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 35bc5793ca71..54a6e1ede1b2 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -128,7 +128,8 @@ function getModifiers( return { type: 'media-size', size: asScreenSize, - } + modifierOrigin: { type: 'tailwind', variant: mediaModifier.value }, + } as StyleMediaSizeModifier }) .filter((m): m is StyleMediaSizeModifier => m != null) } From 2c5ffe4a5f5d9d32b302940360a4770603216cad Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Tue, 3 Dec 2024 16:50:39 +0200 Subject: [PATCH 08/21] fix(editor): get scene data from metadata --- .../canvas-strategies/canvas-strategies.tsx | 2 ++ .../canvas-strategy-types.ts | 1 + .../commands/adjust-css-length-command.ts | 1 + .../canvas/commands/set-css-length-command.ts | 1 + .../select-mode/border-radius-control.tsx | 1 + .../controls/select-mode/flex-gap-control.tsx | 1 + .../select-mode/padding-resize-control.tsx | 1 + .../select-mode/subdued-flex-gap-controls.tsx | 1 + .../select-mode/subdued-padding-control.tsx | 1 + .../plugins/inline-style-plugin.spec.ts | 1 + .../canvas/plugins/style-plugins.ts | 4 +++ .../canvas/plugins/tailwind-style-plugin.ts | 29 ++++++++++++------- .../components/inspector/common/css-utils.ts | 7 ++++- .../inspector/common/property-path-hooks.ts | 11 ++++++- .../src/core/model/element-metadata-utils.ts | 7 +++++ 15 files changed, 56 insertions(+), 13 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx index 5f31076f29bf..caf2eee65b6e 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx @@ -226,6 +226,7 @@ export function pickCanvasStateFromEditorState( propertyControlsInfo: editorState.propertyControlsInfo, styleInfoReader: activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }), } } @@ -253,6 +254,7 @@ export function pickCanvasStateFromEditorStateWithMetadata( propertyControlsInfo: editorState.propertyControlsInfo, styleInfoReader: activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }), } } diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts index 80d553a97a9a..0e98222ddd13 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts @@ -114,6 +114,7 @@ export type StyleInfoReader = (elementPath: ElementPath) => StyleInfo | null export type StyleInfoFactory = (context: { projectContents: ProjectContentTreeRoot + jsxMetadata: ElementInstanceMetadataMap }) => StyleInfoReader export interface InteractionCanvasState { diff --git a/editor/src/components/canvas/commands/adjust-css-length-command.ts b/editor/src/components/canvas/commands/adjust-css-length-command.ts index 128aeac077d6..df4b81ecd292 100644 --- a/editor/src/components/canvas/commands/adjust-css-length-command.ts +++ b/editor/src/components/canvas/commands/adjust-css-length-command.ts @@ -78,6 +78,7 @@ export const runAdjustCssLengthProperties = ( const styleInfoReader = getActivePlugin(withConflictingPropertiesRemoved).styleInfoFactory({ projectContents: withConflictingPropertiesRemoved.projectContents, + jsxMetadata: withConflictingPropertiesRemoved.jsxMetadata, }) const styleInfo = styleInfoReader(command.target) diff --git a/editor/src/components/canvas/commands/set-css-length-command.ts b/editor/src/components/canvas/commands/set-css-length-command.ts index f69f66c328df..cc2a0998f8a2 100644 --- a/editor/src/components/canvas/commands/set-css-length-command.ts +++ b/editor/src/components/canvas/commands/set-css-length-command.ts @@ -87,6 +87,7 @@ export const runSetCssLengthProperty = ( const styleInfo = getActivePlugin(editorStateWithPropsDeleted).styleInfoFactory({ projectContents: editorStateWithPropsDeleted.projectContents, + jsxMetadata: editorStateWithPropsDeleted.jsxMetadata, })(command.target) if (styleInfo == null) { diff --git a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx index eb0d98c4d493..7f01a640b7e8 100644 --- a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx @@ -63,6 +63,7 @@ const borderRadiusSelector = createCachedSelector( (store: StyleInfoSubstate) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), (_: MetadataSubstate, x: ElementPath) => x, (metadata, styleInfoReader, selectedElement) => { diff --git a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx index 672e28d2f801..6f0121923126 100644 --- a/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/flex-gap-control.tsx @@ -139,6 +139,7 @@ export const FlexGapControl = controlForStrategyMemoized((p maybeFlexGapData( getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, })(selectedElement), MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), ), diff --git a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx index c9d8906e69ba..81827667dec2 100644 --- a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx @@ -362,6 +362,7 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo const styleInfoReaderRef = useRefEditorState((store) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), ) diff --git a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx index 539d082cc253..4e0091951593 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-flex-gap-controls.tsx @@ -43,6 +43,7 @@ export const SubduedFlexGapControl = React.memo((pro maybeFlexGapData( getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, })(selectedElement), MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, selectedElement), ), diff --git a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx index 766a180c9386..3fb67c29a496 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx @@ -29,6 +29,7 @@ export const SubduedPaddingControl = React.memo((pro const styleInfoReaderRef = useRefEditorState((store) => getActivePlugin(store.editor).styleInfoFactory({ projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, }), ) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts index 8ba0e9733b8c..e0e8029bce6f 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts @@ -110,6 +110,7 @@ function getStyleInfoFromInlineStyle(editor: EditorRenderResult) { const styleInfoReader = InlineStylePlugin.styleInfoFactory({ projectContents: projectContents, + jsxMetadata: jsxMetadata, }) const styleInfo = styleInfoReader(EP.fromString('sb/scene/div')) return styleInfo diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index 42ecae83b215..206f63d2daff 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -57,6 +57,9 @@ export interface StylePlugin { readStyleFromElementProps: ( attributes: JSXAttributes, prop: T, + context?: { + sceneWidth?: number + }, ) => CSSStyleProperty> | null updateStyles: ( editorState: EditorState, @@ -248,6 +251,7 @@ export function patchRemovedProperties(editorState: EditorState): EditorState { const styleInfoReader = activePlugin.styleInfoFactory({ projectContents: editorState.projectContents, + jsxMetadata: editorState.jsxMetadata, }) const propertiesUpdatedDuringInteraction = getPropertiesUpdatedDuringInteraction(editorState) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 54a6e1ede1b2..6ea6c755e3ab 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -25,6 +25,7 @@ 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 { extractMediaQueryFromCss, mediaQueryToScreenSize } from '../canvas-utils' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' type StyleValueVariants = { value: string | number | undefined @@ -34,8 +35,13 @@ export type TailwindMediaModifier = { type: 'media'; value: string } export type TailwindHoverModifier = { type: 'hover'; value: string } export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier -const parseTailwindPropertyWithConfig = - (config: Config | null) => +const parseTailwindPropertyFactory = + ( + config: Config | null, + context: { + sceneWidth?: number + }, + ) => ( styleDefinition: StyleValueVariants | undefined, prop: T, @@ -57,12 +63,7 @@ const parseTailwindPropertyWithConfig = >, })) - // to be passed somehow - const scene = document.querySelector('[data-testid=remix-scene]') - if (scene == null) { - return null - } - const result = selectValueByBreakpoint(parsed, parseFloat(getComputedStyle(scene).width)) + const result = selectValueByBreakpoint(parsed, context?.sceneWidth) if (result == null) { return null } @@ -232,12 +233,14 @@ const underscoresToSpaces = ( } export const TailwindPlugin = (config: Config | null): StylePlugin => { - const parseTailwindProperty = parseTailwindPropertyWithConfig(config) return { name: 'Tailwind', readStyleFromElementProps:

( attributes: JSXAttributes, prop: P, + context?: { + sceneWidth?: number + }, ): CSSStyleProperty> | null => { const classNameAttribute = defaultEither( null, @@ -252,10 +255,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { } const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config) + const parseTailwindProperty = parseTailwindPropertyFactory(config, context ?? {}) return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop) }, styleInfoFactory: - ({ projectContents }) => + ({ projectContents, jsxMetadata }) => (elementPath) => { const classList = getClassNameAttribute( getElementFromProjectContents(elementPath, projectContents), @@ -266,7 +270,10 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { } const mapping = getTailwindClassMapping(classList.split(' '), config) - + const containingScene = MetadataUtils.getParentSceneMetadata(jsxMetadata, elementPath) + const parseTailwindProperty = parseTailwindPropertyFactory(config, { + sceneWidth: containingScene?.specialSizeMeasurements?.clientWidth, + }) return { gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], 'gap'), flexDirection: parseTailwindProperty( diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 1fdf9f62637f..53fde06d0dfb 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -5997,7 +5997,7 @@ function getMediaModifier( export function selectValueByBreakpoint( parsedVariants: T[], - sceneWidthInPx: number, + sceneWidthInPx?: number, ): T | null { const relevantModifiers = parsedVariants.filter((variant) => { // 1. filter out variants that don't have media modifiers, but keep variants with no modifiers at all @@ -6011,6 +6011,11 @@ export function selectValueByBreakpoint @@ -763,7 +764,15 @@ export function useGetMultiselectedProps

( const styleInfoReaderRef = useRefEditorState( (store) => (props: JSXAttributes, prop: keyof StyleInfo): GetModifiableAttributeResult => { - const elementStyle = getActivePlugin(store.editor).readStyleFromElementProps(props, prop) + // todo: support multiple selected elements + const selectedElement = store.editor.selectedViews[0] + const sceneMetadata = MetadataUtils.getParentSceneMetadata( + store.editor.jsxMetadata, + selectedElement, + ) + const elementStyle = getActivePlugin(store.editor).readStyleFromElementProps(props, prop, { + sceneWidth: sceneMetadata?.specialSizeMeasurements?.clientWidth, + }) if (elementStyle == null) { return right({ type: 'ATTRIBUTE_NOT_FOUND' }) } diff --git a/editor/src/core/model/element-metadata-utils.ts b/editor/src/core/model/element-metadata-utils.ts index 7c3661bbcc77..e9892d1e1c96 100644 --- a/editor/src/core/model/element-metadata-utils.ts +++ b/editor/src/core/model/element-metadata-utils.ts @@ -1061,6 +1061,13 @@ export const MetadataUtils = { const elementMetadata = MetadataUtils.findElementByElementPath(metadata, path) return elementMetadata != null && isSceneFromMetadata(elementMetadata) }, + getParentSceneMetadata( + metadata: ElementInstanceMetadataMap, + path: ElementPath, + ): ElementInstanceMetadata | null { + const parentPath = MetadataUtils.findSceneOfTarget(path, metadata) + return parentPath == null ? null : MetadataUtils.findElementByElementPath(metadata, parentPath) + }, overflows(allElementProps: AllElementProps, path: ElementPath): boolean { const elementProps = allElementProps[EP.toString(path)] ?? {} const styleProps = elementProps.style ?? null From 1e80efe92c903fe7a689891061ad8ea65964e841 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 4 Dec 2024 12:57:30 +0200 Subject: [PATCH 09/21] fix tests for responsive --- .../src/components/canvas/plugins/inline-style-plugin.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts index e0e8029bce6f..054bcd20f9fb 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.spec.ts @@ -43,13 +43,11 @@ export var storyboard = ( const { flexDirection, gap } = styleInfo! expect(flexDirection).toMatchObject({ type: 'property', - tags: [], value: 'column', propertyValue: { value: 'column' }, }) expect(gap).toMatchObject({ type: 'property', - tags: [], value: cssNumber(2, 'rem'), propertyValue: { value: '2rem' }, }) From 7a6e9750d497a5a3cc12e30510c38b1e70fd111c Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Wed, 4 Dec 2024 19:42:46 +0200 Subject: [PATCH 10/21] update correct prop --- .../select-mode/remix-scene-label.tsx | 3 - .../update-class-list.ts | 46 ++++++++-- .../canvas/plugins/tailwind-style-plugin.ts | 43 +++++++++- .../tailwind/tailwind-class-list-utils.ts | 85 ++++++++++++++++--- 4 files changed, 150 insertions(+), 27 deletions(-) diff --git a/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx b/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx index 158cc437b091..52d0281ab262 100644 --- a/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx +++ b/editor/src/components/canvas/controls/select-mode/remix-scene-label.tsx @@ -397,9 +397,6 @@ const DeviceSizeButton = React.memo<{ name: string; sizePx: number }>((props) => } e.preventDefault() e.stopPropagation() - // eslint-disable-next-line - // @ts-ignore - e.stopImmediatePropagation() }} onMouseDown={(e) => e.stopPropagation()} > diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts index b8896c93aa64..e4d97ec9565b 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts @@ -2,7 +2,10 @@ import type { ElementPath } from 'utopia-shared/src/types' import { emptyComments } from 'utopia-shared/src/types' import { mapDropNulls } from '../../../../core/shared/array-utils' import { jsExpressionValue } from '../../../../core/shared/element-template' -import type { PropertiesToUpdate } from '../../../../core/tailwind/tailwind-class-list-utils' +import type { + PropertiesToRemove, + PropertiesToUpdate, +} from '../../../../core/tailwind/tailwind-class-list-utils' import { getParsedClassList, removeClasses, @@ -17,6 +20,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 type { Config } from 'tailwindcss/types/config' +import { getPropertiesToAppliedModifiersMap } from '../tailwind-style-plugin' export type ClassListUpdate = | { type: 'add'; property: string; value: string } @@ -37,27 +41,57 @@ export const runUpdateClassList = ( element: ElementPath, classNameUpdates: ClassListUpdate[], config: Config | null, + context: { + sceneWidth?: number + }, ): EditorStateWithPatch => { const currentClassNameAttribute = getClassNameAttribute(getElementFromProjectContents(element, editorState.projectContents)) ?.value ?? '' + // this will tell for every property which modifiers are in action + const propertyToAppliedModifiersMap = getPropertiesToAppliedModifiersMap( + currentClassNameAttribute, + classNameUpdates.map((update) => update.property), + config, + context, + ) + const parsedClassList = getParsedClassList(currentClassNameAttribute, config) - const propertiesToRemove = mapDropNulls( - (update) => (update.type !== 'remove' ? null : update.property), - classNameUpdates, + const propertiesToRemove: PropertiesToRemove = classNameUpdates.reduce( + (acc: PropertiesToRemove, val) => + val.type === 'remove' + ? { + ...acc, + [val.property]: { + remove: true, + modifiers: propertyToAppliedModifiersMap[val.property] ?? [], + }, + } + : acc, + {}, ) const propertiesToUpdate: PropertiesToUpdate = classNameUpdates.reduce( - (acc: { [property: string]: string }, val) => - val.type === 'remove' ? acc : { ...acc, [val.property]: val.value }, + (acc: PropertiesToUpdate, val) => + val.type === 'remove' + ? acc + : { + ...acc, + [val.property]: { + newValue: val.value, + modifiers: propertyToAppliedModifiersMap[val.property] ?? [], + }, + }, {}, ) const updatedClassList = [ removeClasses(propertiesToRemove), updateExistingClasses(propertiesToUpdate), + // currently we're not adding new breakpoint styles (but only editing current ones), + // so we don't need to pass the propertyToAppliedModifiersMap here addNewClasses(propertiesToUpdate), ].reduce((classList, fn) => fn(classList), parsedClassList) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 6ea6c755e3ab..c7a1663e7d29 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -14,7 +14,7 @@ import type { StyleMediaSizeModifier, StyleModifier, } from '../canvas-types' -import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types' +import { cssStyleProperty, isStyleInfoKey, type CSSStyleProperty } from '../canvas-types' import * as UCL from './tailwind-style-plugin-utils/update-class-list' import { assertNever } from '../../../core/shared/utils' import { @@ -35,7 +35,7 @@ export type TailwindMediaModifier = { type: 'media'; value: string } export type TailwindHoverModifier = { type: 'hover'; value: string } export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier -const parseTailwindPropertyFactory = +export const parseTailwindPropertyFactory = ( config: Config | null, context: { @@ -135,7 +135,7 @@ function getModifiers( .filter((m): m is StyleMediaSizeModifier => m != null) } -const TailwindPropertyMapping: Record = { +export const TailwindPropertyMapping: Record = { left: 'positionLeft', right: 'positionRight', top: 'positionTop', @@ -191,7 +191,7 @@ type TailwindParsedStyle = { value: string variants?: { type: string; value: string }[] } -function getTailwindClassMapping( +export function getTailwindClassMapping( classes: string[], config: Config | null, ): Record { @@ -333,6 +333,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { } }, updateStyles: (editorState, elementPath, updates) => { + const containingScene = MetadataUtils.getParentSceneMetadata( + editorState.jsxMetadata, + elementPath, + ) + const sceneWidth = containingScene?.specialSizeMeasurements?.clientWidth const propsToDelete = mapDropNulls( (update) => update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe @@ -356,7 +361,37 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { elementPath, [...propsToDelete, ...propsToSet], config, + { sceneWidth: sceneWidth }, ) }, } } + +export function getPropertiesToAppliedModifiersMap( + currentClassNameAttribute: string, + propertyNames: string[], + config: Config | null, + context: { + sceneWidth?: number + }, +): Record { + const parseTailwindProperty = parseTailwindPropertyFactory(config, context) + const classMapping = getTailwindClassMapping(currentClassNameAttribute.split(' '), config) + return propertyNames.reduce((acc, propertyName) => { + if (!isStyleInfoKey(propertyName)) { + return acc + } + const parsedProperty = parseTailwindProperty( + classMapping[TailwindPropertyMapping[propertyName]], + propertyName, + ) + if (parsedProperty?.type == 'property' && parsedProperty.appliedModifiers != null) { + return { + ...acc, + [propertyName]: parsedProperty.appliedModifiers, + } + } else { + return acc + } + }, {} as Record) +} diff --git a/editor/src/core/tailwind/tailwind-class-list-utils.ts b/editor/src/core/tailwind/tailwind-class-list-utils.ts index fd6152c79f32..aa191ad779e3 100644 --- a/editor/src/core/tailwind/tailwind-class-list-utils.ts +++ b/editor/src/core/tailwind/tailwind-class-list-utils.ts @@ -1,11 +1,12 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' import type { Config } from 'tailwindcss/types/config' import { mapDropNulls } from '../shared/array-utils' +import type { StyleMediaSizeModifier, StyleModifier } from '../../components/canvas/canvas-types' export type ParsedTailwindClass = { property: string value: string - variants: unknown[] + variants: { type: string; value: string }[] negative: boolean } & Record @@ -49,7 +50,14 @@ export type ClassListTransform = ( ) => TailwindClassParserResult[] export interface PropertiesToUpdate { - [property: string]: string + [property: string]: { newValue: string; modifiers: StyleModifier[] } +} + +export interface PropertiesToRemove { + [property: string]: { + remove: boolean + modifiers: StyleModifier[] + } } export const addNewClasses = @@ -60,12 +68,12 @@ export const addNewClasses = ) const newClasses: TailwindClassParserResult[] = mapDropNulls( - ([prop, value]) => + ([prop, update]) => existingProperties.has(prop) ? null : { type: 'parsed', - ast: { property: prop, value: value, variants: [], negative: false }, + ast: { property: prop, value: update.newValue, variants: [], negative: false }, }, Object.entries(propertiesToAdd), ) @@ -78,30 +86,79 @@ export const updateExistingClasses = (propertiesToUpdate: PropertiesToUpdate): ClassListTransform => (parsedClassList: TailwindClassParserResult[]) => { const classListWithUpdatedClasses: TailwindClassParserResult[] = parsedClassList.map((cls) => { - if (cls.type !== 'parsed' || cls.ast.variants.length > 0) { - return cls - } - const updatedProperty = propertiesToUpdate[cls.ast.property] - if (updatedProperty == null) { + if (cls.type !== 'parsed' || !shouldUpdateClass(cls, propertiesToUpdate)) { return cls } + const propertyToUpdate = propertiesToUpdate[cls.ast.property] return { type: 'parsed', - ast: { property: cls.ast.property, value: updatedProperty, variants: [], negative: false }, + ast: { + property: cls.ast.property, + value: propertyToUpdate.newValue, + variants: cls.ast.variants, + negative: false, + }, } }) return classListWithUpdatedClasses } export const removeClasses = - (propertiesToRemove: string[]): ClassListTransform => + (propertiesToRemove: PropertiesToRemove): ClassListTransform => (parsedClassList: TailwindClassParserResult[]) => { - const propertiesToRemoveSet = new Set(propertiesToRemove) const classListWithRemovedClasses = parsedClassList.filter((cls) => { - if (cls.type !== 'parsed' || cls.ast.variants.length > 0) { + if (cls.type !== 'parsed' || !shouldUpdateClass(cls, propertiesToRemove)) { return cls } - return !propertiesToRemoveSet.has(cls.ast.property) + return !propertiesToRemove[cls.ast.property]?.remove }) return classListWithRemovedClasses } + +function getTailwindSizeVariant(modifiers: StyleModifier[]): string | null { + const mediaModifier = modifiers.find((m): m is StyleMediaSizeModifier => m.type === 'media-size') + if (mediaModifier == null) { + return null + } + if (mediaModifier.modifierOrigin?.type !== 'tailwind') { + return null + } + return mediaModifier.modifierOrigin.variant +} + +function shouldUpdateClass( + cls: TailwindClassParserResult, + propertiesToUpdate: PropertiesToUpdate | PropertiesToRemove, +): boolean { + if (cls.type !== 'parsed') { + return false + } + const propertyToUpdate = propertiesToUpdate[cls.ast.property] + if (propertyToUpdate == null) { + // this property is not in the list + return false + } + const sizeVariantToUpdate = getTailwindSizeVariant(propertyToUpdate.modifiers) + if ( + sizeVariantToUpdate == null && + cls.ast.variants.filter((v) => v.type === 'media').length > 0 + ) { + // we need to update the default property value but this class has size variants + return false + } + if ( + sizeVariantToUpdate != null && + !variantsHasMediaSizeVariant(cls.ast.variants, sizeVariantToUpdate) + ) { + // we need to update a specific size variant but this class doesn't have it + return false + } + return true +} + +function variantsHasMediaSizeVariant( + variants: { type: string; value: string }[], + sizeVariant: string, +): boolean { + return variants.some((v) => v.type === 'media' && v.value === sizeVariant) +} From 7856cf63c79f76ec7c8c5c12eabce132516d0405 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Thu, 5 Dec 2024 01:25:22 +0200 Subject: [PATCH 11/21] feat(responsive): breakpoint aware padding controller --- .../canvas/plugins/tailwind-style-plugin.ts | 8 ++ .../components/inspector/common/css-utils.ts | 13 +++ .../container-subsection/padding-row.tsx | 109 ++++++++++++++++-- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index c7a1663e7d29..74aef1133ffe 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -395,3 +395,11 @@ export function getPropertiesToAppliedModifiersMap( } }, {} as Record) } + +export function getTailwindVariantFromAppliedModifier( + appliedModifier: StyleMediaSizeModifier | null, +): string | null { + return appliedModifier?.modifierOrigin?.type === 'tailwind' + ? appliedModifier.modifierOrigin.variant + : null +} diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 53fde06d0dfb..f3b158e0f0c4 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -86,6 +86,7 @@ import { memoize } from '../../../core/shared/memoize' import * as csstree from 'css-tree' import type { IcnProps } from '../../../uuiui' import { cssNumberEqual } from '../../canvas/controls/select-mode/controls-common' +import type { StyleInfo } from '../../canvas/canvas-types' import type { CompValue, StyleMediaSizeModifier, StyleModifier } from '../../canvas/canvas-types' var combineRegExp = function (regexpList: Array, flags?: string) { @@ -6073,3 +6074,15 @@ export function selectValueByBreakpoint m.type === 'media-size') ?? null + : null +} diff --git a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx index 986035e61e6a..cdf39e40c639 100644 --- a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx +++ b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx @@ -3,6 +3,7 @@ import { useContextSelector } from 'use-context-selector' import { MetadataUtils } from '../../../../../core/model/element-metadata-utils' import { mapArrayToDictionary } from '../../../../../core/shared/array-utils' import type { PropertyPath } from '../../../../../core/shared/project-file-types' +import type { IcnColor } from '../../../../../uuiui' import { Icons } from '../../../../../uuiui' import { useSetHoveredControlsHandlers } from '../../../../canvas/controls/select-mode/select-mode-hooks' import type { SubduedPaddingControlProps } from '../../../../canvas/controls/select-mode/subdued-padding-control' @@ -47,6 +48,10 @@ import { longhandShorthandEventHandler, splitChainedEventValueForProp, } from '../../layout-section/layout-system-subsection/split-chained-number-input' +import { getActivePlugin } from '../../../../canvas/plugins/style-plugins' +import type { StyleInfo } from '../../../../canvas/canvas-types' +import { getAppliedMediaSizeModifierFromBreakpoint } from '../../../common/css-utils' +import { getTailwindVariantFromAppliedModifier } from '../../../../canvas/plugins/tailwind-style-plugin' function buildPaddingPropsToUnset(propertyTarget: ReadonlyArray): Array { return [ @@ -132,6 +137,18 @@ const PaddingControl = React.memo(() => { const shorthand = useInspectorLayoutInfo('padding') const dispatch = useDispatch() + const styleInfo = useRefEditorState((store) => { + const getStyleInfo = getActivePlugin(store.editor).styleInfoFactory({ + projectContents: store.editor.projectContents, + jsxMetadata: store.editor.jsxMetadata, + }) + // TODO: support multiple selected views + return getStyleInfo(store.editor.selectedViews[0]) + }) + const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) + const pathTreesRef = useRefEditorState((store) => store.editor.elementPathTree) + const breakpointColors = getBreakpointColors(styleInfo.current) + const { selectedViewsRef } = useInspectorContext() const canvasControlsForSides = React.useMemo(() => { @@ -177,8 +194,6 @@ const PaddingControl = React.memo(() => { return shorthand.controlStatus === 'simple' || allUnset }, [allUnset, shorthand.controlStatus]) - const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) - const pathTreesRef = useRefEditorState((store) => store.editor.elementPathTree) const startingFrame = MetadataUtils.getFrameOrZeroRect( selectedViewsRef.current[0], metadataRef.current, @@ -325,13 +340,54 @@ const PaddingControl = React.memo(() => { return ( , - horizontal: , - vertical: , - top: , - left: , - bottom: , - right: , + oneValue: ( + + ), + horizontal: ( + + ), + vertical: ( + + ), + top: ( + + ), + left: ( + + ), + bottom: ( + + ), + right: ( + + ), }} tooltips={{ oneValue: 'Padding', @@ -349,3 +405,38 @@ const PaddingControl = React.memo(() => { /> ) }) + +function getBreakpointColors(styleInfo: StyleInfo | null) { + function getStyleFromBreakpoint(prop: keyof StyleInfo): { + color: IcnColor + style: React.CSSProperties + tooltipText?: string + } { + if (styleInfo == null) { + return { color: 'on-highlight-secondary', style: {} } + } + const appliedModifier = getAppliedMediaSizeModifierFromBreakpoint(styleInfo, prop) + const fromBreakpoint = getTailwindVariantFromAppliedModifier(appliedModifier) + if (appliedModifier != null) { + return { + color: 'green', + style: { + backgroundColor: '#90ee9063', + borderRadius: '2px', + }, + tooltipText: + fromBreakpoint != null + ? `This value comes from the ':${fromBreakpoint}' breakpoint` + : undefined, + } + } + return { color: 'on-highlight-secondary', style: {} } + } + return { + padding: getStyleFromBreakpoint('padding'), + paddingTop: getStyleFromBreakpoint('paddingTop'), + paddingRight: getStyleFromBreakpoint('paddingRight'), + paddingBottom: getStyleFromBreakpoint('paddingBottom'), + paddingLeft: getStyleFromBreakpoint('paddingLeft'), + } as const +} From a338c2d4d0b7677916a9012c815efc69726faf97 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 8 Dec 2024 20:24:26 +0200 Subject: [PATCH 12/21] tsc fix --- .../tailwind-class-list-utils.spec.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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..5a149b28ff06 100644 --- a/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts +++ b/editor/src/core/tailwind/tailwind-class-list-utils.spec.ts @@ -141,7 +141,10 @@ describe('tailwind class list utils', () => { describe('removing classes', () => { it('can remove property', () => { const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null) - const updatedClassList = removeClasses(['padding', 'textColor'])(classList) + const updatedClassList = removeClasses({ + padding: { remove: true, modifiers: [] }, + textColor: { remove: true, modifiers: [] }, + })(classList) expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot( `"m-2 w-4 flex flex-row"`, ) @@ -151,7 +154,10 @@ describe('tailwind class list utils', () => { 'p-4 m-2 text-white hover:text-red-100 w-4 flex flex-row', null, ) - const updatedClassList = removeClasses(['padding', 'textColor'])(classList) + const updatedClassList = removeClasses({ + padding: { remove: true, modifiers: [] }, + textColor: { remove: true, modifiers: [] }, + })(classList) expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot( `"m-2 hover:text-red-100 w-4 flex flex-row"`, ) @@ -162,8 +168,8 @@ describe('tailwind class list utils', () => { it('can update class in class list', () => { const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null) const updatedClassList = updateExistingClasses({ - flexDirection: 'column', - width: '23px', + flexDirection: { newValue: 'column', modifiers: [] }, + width: { newValue: '23px', modifiers: [] }, })(classList) expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot( `"p-4 m-2 text-white w-[23px] flex flex-col"`, @@ -171,7 +177,9 @@ describe('tailwind class list utils', () => { }) it('does not remove property with selector', () => { const classList = getParsedClassList('p-4 hover:p-6 m-2 text-white w-4 flex flex-row', null) - const updatedClassList = updateExistingClasses({ padding: '8rem' })(classList) + const updatedClassList = updateExistingClasses({ + padding: { newValue: '8rem', modifiers: [] }, + })(classList) expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot( `"p-32 hover:p-6 m-2 text-white w-4 flex flex-row"`, ) @@ -182,9 +190,9 @@ describe('tailwind class list utils', () => { it('can add new class to class list', () => { const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null) const updatedClassList = addNewClasses({ - backgroundColor: 'white', - justifyContent: 'space-between', - positionLeft: '-20px', + backgroundColor: { newValue: 'white', modifiers: [] }, + justifyContent: { newValue: 'space-between', modifiers: [] }, + positionLeft: { newValue: '-20px', modifiers: [] }, })(classList) expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot( `"p-4 m-2 text-white w-4 flex flex-row bg-white justify-between -left-[20px]"`, From b3f99342d45e1f27777f114f099bf9aa44656c36 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 8 Dec 2024 22:37:35 +0200 Subject: [PATCH 13/21] cache screens to media query mapping --- .../update-class-list.ts | 2 +- .../plugins/tailwind-style-plugin.spec.ts | 1 + .../canvas/plugins/tailwind-style-plugin.ts | 105 +----------------- 3 files changed, 5 insertions(+), 103 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts index e4d97ec9565b..7ef7754cf47c 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts @@ -20,7 +20,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 type { Config } from 'tailwindcss/types/config' -import { getPropertiesToAppliedModifiersMap } from '../tailwind-style-plugin' +import { getPropertiesToAppliedModifiersMap } from './tailwind-media-query-utils' export type ClassListUpdate = | { type: 'add'; property: string; value: string } diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts index cf1e604a3833..2919bf9eb5c9 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts @@ -9,6 +9,7 @@ import { TailwindPlugin } from './tailwind-style-plugin' import { createModifiedProject } from '../../../sample-projects/sample-project-utils.test-utils' import { TailwindConfigPath } from '../../../core/tailwind/tailwind-config' import { getTailwindConfigCached } from '../../../core/tailwind/tailwind-compilation' +import type { Config } from 'tailwindcss/types/config' const Project = createModifiedProject({ [StoryboardFilePath]: ` diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 74aef1133ffe..3eb1a137ee87 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -8,13 +8,8 @@ import { cssParsers, selectValueByBreakpoint } from '../../inspector/common/css- import { mapDropNulls } from '../../../core/shared/array-utils' import type { StylePlugin } from './style-plugins' import type { Config } from 'tailwindcss/types/config' -import type { - ParsedVariant, - StyleInfo, - StyleMediaSizeModifier, - StyleModifier, -} from '../canvas-types' -import { cssStyleProperty, isStyleInfoKey, type CSSStyleProperty } from '../canvas-types' +import type { ParsedVariant, StyleInfo } 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 { @@ -24,8 +19,8 @@ import { 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 { extractMediaQueryFromCss, mediaQueryToScreenSize } from '../canvas-utils' import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { getModifiers } from './tailwind-style-plugin-utils/tailwind-media-query-utils' type StyleValueVariants = { value: string | number | undefined @@ -78,63 +73,6 @@ export const parseTailwindPropertyFactory = ) } -type TailwindScreen = string | { min: string; max: string } - -/** - * This function gets variants in the form of {type: 'media', value: 'sm'} - * and turns them into modifiers in the form of [{type: 'media-size', size: {min: {value: 0, unit: 'px'}, max: {value: 100, unit: 'em'}}}] - * according to the tailwind config - */ -function getModifiers( - variants: { type: string; value: string }[], - config: Config | null, -): StyleModifier[] { - // media modifiers - const mediaModifiers = variants.filter((v) => v.type === 'media') - const screenSizes: Record = (config?.theme?.screens as Record< - string, - TailwindScreen - >) ?? { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - '2xl': '1536px', - ...((config?.theme?.extend?.screens as Record) ?? {}), - } - return mediaModifiers - .map((mediaModifier) => { - const size: string | { min: string; max: string } | undefined = - screenSizes[mediaModifier.value as keyof typeof screenSizes] - if (size == null) { - return null - } - const mediaString = - typeof size === 'string' - ? `@media (min-width: ${size})` - : `@media ${[ - size.min != null ? `(min-width: ${size.min})` : '', - size.max != null ? `(max-width: ${size.max})` : '', - ] - .filter((s) => s != '') - .join(' and ')}` - const parsed = extractMediaQueryFromCss(mediaString) - if (parsed == null) { - return null - } - const asScreenSize = mediaQueryToScreenSize(parsed) - if (asScreenSize == null) { - return null - } - return { - type: 'media-size', - size: asScreenSize, - modifierOrigin: { type: 'tailwind', variant: mediaModifier.value }, - } as StyleMediaSizeModifier - }) - .filter((m): m is StyleMediaSizeModifier => m != null) -} - export const TailwindPropertyMapping: Record = { left: 'positionLeft', right: 'positionRight', @@ -366,40 +304,3 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { }, } } - -export function getPropertiesToAppliedModifiersMap( - currentClassNameAttribute: string, - propertyNames: string[], - config: Config | null, - context: { - sceneWidth?: number - }, -): Record { - const parseTailwindProperty = parseTailwindPropertyFactory(config, context) - const classMapping = getTailwindClassMapping(currentClassNameAttribute.split(' '), config) - return propertyNames.reduce((acc, propertyName) => { - if (!isStyleInfoKey(propertyName)) { - return acc - } - const parsedProperty = parseTailwindProperty( - classMapping[TailwindPropertyMapping[propertyName]], - propertyName, - ) - if (parsedProperty?.type == 'property' && parsedProperty.appliedModifiers != null) { - return { - ...acc, - [propertyName]: parsedProperty.appliedModifiers, - } - } else { - return acc - } - }, {} as Record) -} - -export function getTailwindVariantFromAppliedModifier( - appliedModifier: StyleMediaSizeModifier | null, -): string | null { - return appliedModifier?.modifierOrigin?.type === 'tailwind' - ? appliedModifier.modifierOrigin.variant - : null -} From 166919287411ee0ef7c4f215cb45f0cb0a733217 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Sun, 8 Dec 2024 23:59:30 +0200 Subject: [PATCH 14/21] refactor for better cache --- .../canvas/canvas-utils-unit-tests.spec.tsx | 78 +++++- editor/src/components/canvas/canvas-utils.ts | 17 ++ .../tailwind-media-query-utils.spec.ts | 229 ++++++++++++++++++ .../tailwind-media-query-utils.ts | 120 +++++++++ .../update-class-list.ts | 1 - 5 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.spec.ts create mode 100644 editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts diff --git a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx index 417540fe6d11..99b6a6c9b4ed 100644 --- a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx +++ b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx @@ -10,7 +10,11 @@ import { singleResizeChange, pinMoveChange, pinFrameChange } from './canvas-type import type { CanvasVector } from '../../core/shared/math-utils' import { canvasRectangle } from '../../core/shared/math-utils' import type { MediaQuery } from './canvas-utils' -import { mediaQueryToScreenSize, updateFramesOfScenesAndComponents } from './canvas-utils' +import { + extractScreenSizeFromCss, + mediaQueryToScreenSize, + updateFramesOfScenesAndComponents, +} from './canvas-utils' import * as csstree from 'css-tree' import { NO_OP } from '../../core/shared/utils' import { editorModelFromPersistentModel } from '../editor/store/editor-state' @@ -526,3 +530,75 @@ describe('mediaQueryToScreenSize', () => { }) }) }) + +describe('extractScreenSizeFromCss', () => { + beforeEach(() => { + // Clear the cache before each test + ;(extractScreenSizeFromCss as any).screenSizeCache?.clear() + }) + + it('extracts screen size from simple media query', () => { + const css = '@media (min-width: 100px) and (max-width: 500px)' + const result = extractScreenSizeFromCss(css) + expect(result).toEqual({ + min: { value: 100, unit: 'px' }, + max: { value: 500, unit: 'px' }, + }) + }) + + it('returns null for invalid media query', () => { + const css = 'not-a-media-query' + const result = extractScreenSizeFromCss(css) + expect(result).toBeNull() + }) + + it('uses cache for repeated calls with same CSS', () => { + const css = '@media (min-width: 100px)' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same object reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) // Use toBe for reference equality + expect(result1).toEqual({ + min: { value: 100, unit: 'px' }, + }) + }) + + it('caches null results', () => { + const css = 'invalid-css' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same null reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) + expect(result1).toBeNull() + }) + + it('handles different CSS strings independently in cache', () => { + const css1 = '@media (min-width: 100px)' + const css2 = '@media (max-width: 500px)' + + // First string + const result1a = extractScreenSizeFromCss(css1) + const result1b = extractScreenSizeFromCss(css1) + expect(result1a).toBe(result1b) + expect(result1a).toEqual({ + min: { value: 100, unit: 'px' }, + }) + + // Second string + const result2a = extractScreenSizeFromCss(css2) + const result2b = extractScreenSizeFromCss(css2) + expect(result2a).toBe(result2b) + expect(result2a).toEqual({ + max: { value: 500, unit: 'px' }, + }) + + // Different strings should have different references + expect(result1a).not.toBe(result2a) + }) +}) diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 487dde4672b6..3a3cadb7e5de 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -2412,3 +2412,20 @@ export function extractMediaQueryFromCss(css: string): MediaQuery | null { }) return result } + +// Cache for storing previously parsed screen sizes +const screenSizeCache: Map = new Map() +export function extractScreenSizeFromCss(css: string): ScreenSize | null { + // Check cache first + const cached = screenSizeCache.get(css) + if (cached !== undefined) { + return cached + } + + // If not in cache, compute and store result + const mediaQuery = extractMediaQueryFromCss(css) + const result = mediaQuery == null ? null : mediaQueryToScreenSize(mediaQuery) + + screenSizeCache.set(css, result) + return result +} diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.spec.ts new file mode 100644 index 000000000000..6b8482d65fdb --- /dev/null +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.spec.ts @@ -0,0 +1,229 @@ +import type { Config } from 'tailwindcss' +import { getModifiers, screensConfigToScreenSizes } from './tailwind-media-query-utils' + +describe('getModifiers', () => { + it('returns empty array for non-media variants', () => { + const variants = [{ type: 'hover', value: 'hover' }] + const config: Config = { theme: { screens: {} } } as Config + + const result = getModifiers(variants, config) + expect(result).toEqual([]) + }) + + it('handles default screen sizes correctly', () => { + const variants = [{ type: 'media', value: 'md' }] + const config = null // null config should use defaults + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 768, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'md' }, + }, + ]) + }) + + it('handles custom screen sizes from config', () => { + const variants = [{ type: 'media', value: 'custom' }] + const config = { + theme: { + screens: { + custom: '1000px', + }, + }, + } as unknown as Config + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 1000, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'custom' }, + }, + ]) + }) + + it('handles min-max range screen sizes', () => { + const variants = [{ type: 'media', value: 'tablet' }] + const config = { + theme: { + screens: { + tablet: { min: '768px', max: '1024px' }, + }, + }, + } as unknown as Config + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 768, unit: 'px' }, + max: { value: 1024, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'tablet' }, + }, + ]) + }) + + it('handles extended screen sizes', () => { + const variants = [{ type: 'media', value: 'extra' }] + const config = { + theme: { + screens: { + sm: '640px', + }, + extend: { + screens: { + extra: '1400px', + }, + }, + }, + } as unknown as Config + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 1400, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'extra' }, + }, + ]) + }) + + it('handles multiple media variants', () => { + const variants = [ + { type: 'media', value: 'sm' }, + { type: 'media', value: 'lg' }, + ] + const config = null // use defaults + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 640, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'sm' }, + }, + { + type: 'media-size', + size: { + min: { value: 1024, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'lg' }, + }, + ]) + }) + + it('filters out invalid screen sizes', () => { + const variants = [ + { type: 'media', value: 'invalid' }, + { type: 'media', value: 'md' }, + ] + const config = null // use defaults + + const result = getModifiers(variants, config) + expect(result).toEqual([ + { + type: 'media-size', + size: { + min: { value: 768, unit: 'px' }, + }, + modifierOrigin: { type: 'tailwind', variant: 'md' }, + }, + ]) + }) +}) + +describe('screensConfigToScreenSizes', () => { + it('returns default screen sizes when config is null', () => { + const result = screensConfigToScreenSizes(null) + expect(result).toEqual({ + sm: { min: { value: 640, unit: 'px' } }, + md: { min: { value: 768, unit: 'px' } }, + lg: { min: { value: 1024, unit: 'px' } }, + xl: { min: { value: 1280, unit: 'px' } }, + '2xl': { min: { value: 1536, unit: 'px' } }, + }) + }) + + it('handles custom screen sizes', () => { + const config = { + theme: { + screens: { + mobile: '400px', + tablet: '800px', + }, + }, + } as unknown as Config + + const result = screensConfigToScreenSizes(config) + expect(result).toEqual({ + mobile: { min: { value: 400, unit: 'px' } }, + tablet: { min: { value: 800, unit: 'px' } }, + }) + }) + + it('handles min-max range screen sizes', () => { + const config = { + theme: { + screens: { + tablet: { min: '768px', max: '1024px' }, + }, + }, + } as unknown as Config + + const result = screensConfigToScreenSizes(config) + expect(result).toEqual({ + tablet: { + min: { value: 768, unit: 'px' }, + max: { value: 1024, unit: 'px' }, + }, + }) + }) + + it('merges extended screen sizes with base config', () => { + const config = { + theme: { + screens: { + sm: '640px', + }, + extend: { + screens: { + custom: '1400px', + }, + }, + }, + } as unknown as Config + + const result = screensConfigToScreenSizes(config) + expect(result).toEqual({ + sm: { min: { value: 640, unit: 'px' } }, + custom: { min: { value: 1400, unit: 'px' } }, + }) + }) + + it('handles empty config objects', () => { + const config = { + theme: {}, + } as unknown as Config + + const result = screensConfigToScreenSizes(config) + expect(result).toEqual({ + sm: { min: { value: 640, unit: 'px' } }, + md: { min: { value: 768, unit: 'px' } }, + lg: { min: { value: 1024, unit: 'px' } }, + xl: { min: { value: 1280, unit: 'px' } }, + '2xl': { min: { value: 1536, unit: 'px' } }, + }) + }) +}) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts new file mode 100644 index 000000000000..a7a612411c41 --- /dev/null +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts @@ -0,0 +1,120 @@ +import type { Config } from 'tailwindcss/types/config' +import { + isStyleInfoKey, + type ScreenSize, + type StyleMediaSizeModifier, + type StyleModifier, +} from '../../canvas-types' +import { extractScreenSizeFromCss } from '../../canvas-utils' +import { TailwindPropertyMapping } from '../tailwind-style-plugin' +import { parseTailwindPropertyFactory } from '../tailwind-style-plugin' +import { getTailwindClassMapping } from '../tailwind-style-plugin' + +export const TAILWIND_DEFAULT_SCREENS = { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} +const defaultTailwindConfig = { + theme: { + screens: TAILWIND_DEFAULT_SCREENS, + }, +} as unknown as Config +type TailwindScreen = string | { min: string; max: string } + +export function screensConfigToScreenSizes(config: Config | null): Record { + const tailwindConfig = config ?? defaultTailwindConfig + const screenSizes: Record = { + ...((tailwindConfig.theme?.screens as Record) ?? + TAILWIND_DEFAULT_SCREENS), + ...((tailwindConfig.theme?.extend?.screens as Record) ?? {}), + } + + return Object.fromEntries( + Object.entries(screenSizes) + .map(([key, size]) => { + const mediaString = + typeof size === 'string' + ? `@media (min-width: ${size})` + : `@media ${[ + size.min != null ? `(min-width: ${size.min})` : '', + size.max != null ? `(max-width: ${size.max})` : '', + ] + .filter((s) => s != '') + .join(' and ')}` + + const screenSize = extractScreenSizeFromCss(mediaString) + if (screenSize == null) { + return null + } + return [key, screenSize] + }) + .filter((entry): entry is [string, ScreenSize] => entry != null), + ) +} + +/** + * This function gets variants in the form of {type: 'media', value: 'sm'} + * and turns them into modifiers in the form of [{type: 'media-size', size: {min: {value: 0, unit: 'px'}, max: {value: 100, unit: 'em'}}}] + * according to the tailwind config + */ +export function getModifiers( + variants: { type: string; value: string }[], + config: Config | null, +): StyleModifier[] { + const mediaModifiers = variants.filter((v) => v.type === 'media') + const screenSizes = screensConfigToScreenSizes(config) + + return mediaModifiers + .map((mediaModifier) => { + const size = screenSizes[mediaModifier.value] + if (size == null) { + return null + } + return { + type: 'media-size', + size: size, + modifierOrigin: { type: 'tailwind', variant: mediaModifier.value }, + } as StyleMediaSizeModifier + }) + .filter((m): m is StyleMediaSizeModifier => m != null) +} + +export function getPropertiesToAppliedModifiersMap( + currentClassNameAttribute: string, + propertyNames: string[], + config: Config | null, + context: { + sceneWidth?: number + }, +): Record { + const parseTailwindProperty = parseTailwindPropertyFactory(config, context) + const classMapping = getTailwindClassMapping(currentClassNameAttribute.split(' '), config) + return propertyNames.reduce((acc, propertyName) => { + if (!isStyleInfoKey(propertyName)) { + return acc + } + const parsedProperty = parseTailwindProperty( + classMapping[TailwindPropertyMapping[propertyName]], + propertyName, + ) + if (parsedProperty?.type == 'property' && parsedProperty.appliedModifiers != null) { + return { + ...acc, + [propertyName]: parsedProperty.appliedModifiers, + } + } else { + return acc + } + }, {} as Record) +} + +export function getTailwindVariantFromAppliedModifier( + appliedModifier: StyleMediaSizeModifier | null, +): string | null { + return appliedModifier?.modifierOrigin?.type === 'tailwind' + ? appliedModifier.modifierOrigin.variant + : null +} diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts index 7ef7754cf47c..44b3e0c2e351 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts @@ -1,6 +1,5 @@ import type { ElementPath } from 'utopia-shared/src/types' import { emptyComments } from 'utopia-shared/src/types' -import { mapDropNulls } from '../../../../core/shared/array-utils' import { jsExpressionValue } from '../../../../core/shared/element-template' import type { PropertiesToRemove, From 819cf3b9f40180d6eb17260cbe3999ddc6682096 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 11:58:20 +0200 Subject: [PATCH 15/21] bring correct import --- .../sections/style-section/container-subsection/padding-row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx index cdf39e40c639..411211ef8f96 100644 --- a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx +++ b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx @@ -51,7 +51,7 @@ import { import { getActivePlugin } from '../../../../canvas/plugins/style-plugins' import type { StyleInfo } from '../../../../canvas/canvas-types' import { getAppliedMediaSizeModifierFromBreakpoint } from '../../../common/css-utils' -import { getTailwindVariantFromAppliedModifier } from '../../../../canvas/plugins/tailwind-style-plugin' +import { getTailwindVariantFromAppliedModifier } from '../../../../canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils' function buildPaddingPropsToUnset(propertyTarget: ReadonlyArray): Array { return [ From 4eb8de7ee628c9384b038c5a6405c80e52b19a6f Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:06:57 +0200 Subject: [PATCH 16/21] refactor --- editor/src/components/canvas/canvas-types.ts | 44 ++-- .../canvas/canvas-utils-unit-tests.spec.tsx | 117 +-------- editor/src/components/canvas/canvas-utils.ts | 169 ------------- .../canvas/commands/utils/property-utils.ts | 4 +- .../canvas/plugins/inline-style-plugin.ts | 3 +- ...c.ts => tailwind-responsive-utils.spec.ts} | 2 +- ...-utils.ts => tailwind-responsive-utils.ts} | 14 +- .../update-class-list.ts | 2 +- .../canvas/plugins/tailwind-style-plugin.ts | 23 +- .../src/components/canvas/responsive-types.ts | 39 +++ .../canvas/responsive-utils.spec.ts | 168 +++++++++++++ .../src/components/canvas/responsive-utils.ts | 227 ++++++++++++++++++ .../inspector/common/css-utils.spec.ts | 57 ----- .../components/inspector/common/css-utils.ts | 103 +------- .../container-subsection/padding-row.tsx | 4 +- 15 files changed, 477 insertions(+), 499 deletions(-) rename editor/src/components/canvas/plugins/tailwind-style-plugin-utils/{tailwind-media-query-utils.spec.ts => tailwind-responsive-utils.spec.ts} (99%) rename editor/src/components/canvas/plugins/tailwind-style-plugin-utils/{tailwind-media-query-utils.ts => tailwind-responsive-utils.ts} (90%) create mode 100644 editor/src/components/canvas/responsive-types.ts create mode 100644 editor/src/components/canvas/responsive-utils.spec.ts create mode 100644 editor/src/components/canvas/responsive-utils.ts diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 1fb68623ee33..aa909333c04c 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -31,6 +31,7 @@ import type { FlexDirection, ParsedCSSProperties, } from '../inspector/common/css-utils' +import type { ScreenSize } from './responsive-types' export const CanvasContainerID = 'canvas-container' export const SceneContainerName = 'scene' @@ -554,12 +555,8 @@ interface CSSStylePropertyNotParsable { interface ParsedCSSStyleProperty { type: 'property' propertyValue: JSExpression | PartOfJSXAttributeValue - value: T - appliedModifiers?: StyleModifier[] - variants?: { - value: T - modifiers?: StyleModifier[] - }[] + currentVariant: CSSVariant + variants?: CSSVariant[] } type StyleModifierMetadata = { type: string; modifierOrigin?: StyleModifierOrigin } @@ -579,17 +576,6 @@ export type ParsedVariant = { modifiers?: StyleModifier[] } -export type CompValue = { - value: number - unit: string -} - -// @media (min-width: 100px) and (max-width: 200em) => { min: { value: 100, unit: 'px' }, max: { value: 200, unit: 'em' } } -export type ScreenSize = { - min?: CompValue - max?: CompValue -} - export type CSSStyleProperty = | CSSStylePropertyNotFound | CSSStylePropertyNotParsable @@ -605,21 +591,25 @@ export function cssStylePropertyNotParsable( return { type: 'not-parsable', originalValue: originalValue } } +export type CSSVariant = { + value: T + modifiers?: StyleModifier[] +} + +export function cssVariant(value: T, modifiers?: StyleModifier[]): CSSVariant { + return { value: value, modifiers: modifiers } +} + export function cssStyleProperty( - value: T, propertyValue: JSExpression | PartOfJSXAttributeValue, - appliedModifiers?: StyleModifier[], - variants?: { - value: T - modifiers?: StyleModifier[] - }[], + currentVariant: CSSVariant, + variants?: CSSVariant[], ): ParsedCSSStyleProperty { return { type: 'property', - variants: variants ?? [], - value: value, propertyValue: propertyValue, - appliedModifiers: appliedModifiers, + currentVariant: currentVariant, + variants: variants ?? [], } } @@ -629,7 +619,7 @@ export function screenSizeModifier(size: ScreenSize): StyleMediaSizeModifier { export function maybePropertyValue(property: CSSStyleProperty): T | null { if (property.type === 'property') { - return property.value + return property.currentVariant.value } return null } diff --git a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx index 99b6a6c9b4ed..120da8d8fb17 100644 --- a/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx +++ b/editor/src/components/canvas/canvas-utils-unit-tests.spec.tsx @@ -5,17 +5,11 @@ import { testPrintCodeFromEditorState, getEditorState, } from './ui-jsx.test-utils' -import type { EdgePosition, ScreenSize } from './canvas-types' +import type { EdgePosition } from './canvas-types' import { singleResizeChange, pinMoveChange, pinFrameChange } from './canvas-types' import type { CanvasVector } from '../../core/shared/math-utils' import { canvasRectangle } from '../../core/shared/math-utils' -import type { MediaQuery } from './canvas-utils' -import { - extractScreenSizeFromCss, - mediaQueryToScreenSize, - updateFramesOfScenesAndComponents, -} from './canvas-utils' -import * as csstree from 'css-tree' +import { updateFramesOfScenesAndComponents } from './canvas-utils' import { NO_OP } from '../../core/shared/utils' import { editorModelFromPersistentModel } from '../editor/store/editor-state' import { complexDefaultProjectPreParsed } from '../../sample-projects/sample-project-utils.test-utils' @@ -495,110 +489,3 @@ describe('updateFramesOfScenesAndComponents - pinFrameChange -', () => { ) }) }) - -describe('mediaQueryToScreenSize', () => { - it('converts simple screen size queries', () => { - const testCases: { input: string; expected: ScreenSize }[] = [ - { - input: '@media (100px 100px)', - expected: { min: { value: 100, unit: 'px' } }, - }, - ] - testCases.forEach((testCase) => { - csstree.walk(csstree.parse(testCase.input), (node) => { - if (node.type === 'MediaQuery') { - const result = mediaQueryToScreenSize(node as unknown as MediaQuery) - expect(result).toEqual(testCase.expected) - } - }) - }) - }) -}) - -describe('extractScreenSizeFromCss', () => { - beforeEach(() => { - // Clear the cache before each test - ;(extractScreenSizeFromCss as any).screenSizeCache?.clear() - }) - - it('extracts screen size from simple media query', () => { - const css = '@media (min-width: 100px) and (max-width: 500px)' - const result = extractScreenSizeFromCss(css) - expect(result).toEqual({ - min: { value: 100, unit: 'px' }, - max: { value: 500, unit: 'px' }, - }) - }) - - it('returns null for invalid media query', () => { - const css = 'not-a-media-query' - const result = extractScreenSizeFromCss(css) - expect(result).toBeNull() - }) - - it('uses cache for repeated calls with same CSS', () => { - const css = '@media (min-width: 100px)' - - // First call - const result1 = extractScreenSizeFromCss(css) - // Second call - should return same object reference - const result2 = extractScreenSizeFromCss(css) - - expect(result1).toBe(result2) // Use toBe for reference equality - expect(result1).toEqual({ - min: { value: 100, unit: 'px' }, - }) - }) - - it('caches null results', () => { - const css = 'invalid-css' - - // First call - const result1 = extractScreenSizeFromCss(css) - // Second call - should return same null reference - const result2 = extractScreenSizeFromCss(css) - - expect(result1).toBe(result2) - expect(result1).toBeNull() - }) - - it('handles different CSS strings independently in cache', () => { - const css1 = '@media (min-width: 100px)' - const css2 = '@media (max-width: 500px)' - - // First string - const result1a = extractScreenSizeFromCss(css1) - const result1b = extractScreenSizeFromCss(css1) - expect(result1a).toBe(result1b) - expect(result1a).toEqual({ - min: { value: 100, unit: 'px' }, - }) - - // Second string - const result2a = extractScreenSizeFromCss(css2) - const result2b = extractScreenSizeFromCss(css2) - expect(result2a).toBe(result2b) - expect(result2a).toEqual({ - max: { value: 500, unit: 'px' }, - }) - - // Different strings should have different references - expect(result1a).not.toBe(result2a) - }) -}) diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 3a3cadb7e5de..2bfd96433fdc 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -1,4 +1,3 @@ -import * as csstree from 'css-tree' import type { DataRouteObject } from 'react-router' import type { LayoutPinnedProp, LayoutTargetableProp } from '../../core/layout/layout-helpers-new' import { @@ -126,11 +125,9 @@ import * as EP from '../../core/shared/element-path' import * as PP from '../../core/shared/property-path' import type { CanvasFrameAndTarget, - CompValue, DuplicateNewUID, EdgePosition, PinOrFlexFrameChange, - ScreenSize, } from './canvas-types' import { flexResizeChange, pinFrameChange } from './canvas-types' import { @@ -2263,169 +2260,3 @@ export function projectContentsSameForRefreshRequire( // If nothing differs, return true. return true } - -export interface MediaQuery { - type: 'MediaQuery' - loc: null - modifier: null - mediaType: null - condition?: { - type: 'Condition' - loc: null - kind: 'media' - children: Array - } -} - -interface FeatureRange { - type: 'FeatureRange' - loc: null - kind: 'media' - left?: csstree.Dimension | csstree.Identifier - leftComparison: '<' | '>' - middle: csstree.Dimension | csstree.Identifier - rightComparison: '<' | '>' - right?: csstree.Dimension | csstree.Identifier -} - -interface Feature { - type: 'Feature' - loc: null - kind: 'media' - name: 'min-width' | 'max-width' - value?: csstree.Dimension -} - -function extractFromFeatureRange(featureRange: FeatureRange): { - leftValue: CompValue | null - rightValue: CompValue | null - leftComparison: '<' | '>' | null - rightComparison: '<' | '>' | null -} | null { - // 100px < width < 500px or 500px > width > 100px or 100px > width or 500px < width - if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { - const leftValue = - featureRange.left?.type === 'Dimension' - ? { - value: Number(featureRange.left.value), - unit: featureRange.left.unit, - } - : null - - const rightValue = - featureRange.right?.type === 'Dimension' - ? { - value: Number(featureRange.right.value), - unit: featureRange.right.unit, - } - : null - - return { - leftValue: leftValue, - rightValue: rightValue, - leftComparison: featureRange.leftComparison, - rightComparison: featureRange.rightComparison, - } - } - // width > 100px or width < 500px - if (featureRange?.left?.type === 'Identifier' && featureRange.left.name === 'width') { - const rightValue = - featureRange.middle?.type === 'Dimension' - ? { - value: Number(featureRange.middle.value), - unit: featureRange.middle.unit, - } - : null - // this is not a mistake, since we normalize the "width" to be in the middle - const rightComparison = featureRange.leftComparison - - return { - leftValue: null, - leftComparison: null, - rightValue: rightValue, - rightComparison: rightComparison, - } - } - return null -} -export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { - const result: ScreenSize = {} - - if (mediaQuery.condition?.type === 'Condition') { - // Handle FeatureRange case - const featureRanges = mediaQuery.condition.children.filter( - (child): child is FeatureRange => child.type === 'FeatureRange', - ) as Array - - featureRanges.forEach((featureRange) => { - const rangeData = extractFromFeatureRange(featureRange) - if (rangeData == null) { - return - } - const { leftValue, rightValue, leftComparison, rightComparison } = rangeData - if (leftValue != null) { - if (leftComparison === '<') { - result.min = leftValue - } else { - result.max = leftValue - } - } - if (rightValue != null) { - if (rightComparison === '<') { - result.max = rightValue - } else { - result.min = rightValue - } - } - }) - - // Handle Feature case (min-width/max-width) - const features = mediaQuery.condition.children.filter( - (child): child is Feature => child.type === 'Feature', - ) - features.forEach((feature) => { - if (feature.value?.type === 'Dimension') { - if (feature.name === 'min-width') { - result.min = { - value: Number(feature.value.value), - unit: feature.value.unit, - } - } else if (feature.name === 'max-width') { - result.max = { - value: Number(feature.value.value), - unit: feature.value.unit, - } - } - } - }) - } - - return result -} - -export function extractMediaQueryFromCss(css: string): MediaQuery | null { - let result: MediaQuery | null = null - csstree.walk(csstree.parse(css), (node) => { - if (node.type === 'MediaQuery') { - result = node as unknown as MediaQuery - } - }) - return result -} - -// Cache for storing previously parsed screen sizes -const screenSizeCache: Map = new Map() -export function extractScreenSizeFromCss(css: string): ScreenSize | null { - // Check cache first - const cached = screenSizeCache.get(css) - if (cached !== undefined) { - return cached - } - - // If not in cache, compute and store result - const mediaQuery = extractMediaQueryFromCss(css) - const result = mediaQuery == null ? null : mediaQueryToScreenSize(mediaQuery) - - screenSizeCache.set(css, result) - return result -} diff --git a/editor/src/components/canvas/commands/utils/property-utils.ts b/editor/src/components/canvas/commands/utils/property-utils.ts index 123bba748aae..b0804b3d48bc 100644 --- a/editor/src/components/canvas/commands/utils/property-utils.ts +++ b/editor/src/components/canvas/commands/utils/property-utils.ts @@ -117,8 +117,8 @@ export function getCSSNumberFromStyleInfo( return { type: 'not-found' } } - if (prop.type === 'not-parsable' || !isCSSNumber(prop.value)) { + if (prop.type === 'not-parsable' || !isCSSNumber(prop.currentVariant.value)) { return { type: 'not-css-number' } } - return { type: 'css-number', number: prop.value } + return { type: 'css-number', number: prop.currentVariant.value } } diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index d8c71ec114dc..8546bcb74b94 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -13,6 +13,7 @@ import { cssStyleProperty, cssStylePropertyNotParsable, cssStylePropertyNotFound, + cssVariant, } from '../canvas-types' import { mapDropNulls } from '../../../core/shared/array-utils' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' @@ -45,7 +46,7 @@ function getPropertyFromInstance

{ it('returns empty array for non-media variants', () => { diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts similarity index 90% rename from editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts rename to editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts index a7a612411c41..7cda77cbb34f 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts @@ -1,11 +1,7 @@ import type { Config } from 'tailwindcss/types/config' -import { - isStyleInfoKey, - type ScreenSize, - type StyleMediaSizeModifier, - type StyleModifier, -} from '../../canvas-types' -import { extractScreenSizeFromCss } from '../../canvas-utils' +import { isStyleInfoKey, type StyleMediaSizeModifier, type StyleModifier } from '../../canvas-types' +import type { ScreenSize } from '../../responsive-types' +import { extractScreenSizeFromCss } from '../../responsive-utils' import { TailwindPropertyMapping } from '../tailwind-style-plugin' import { parseTailwindPropertyFactory } from '../tailwind-style-plugin' import { getTailwindClassMapping } from '../tailwind-style-plugin' @@ -100,10 +96,10 @@ export function getPropertiesToAppliedModifiersMap( classMapping[TailwindPropertyMapping[propertyName]], propertyName, ) - if (parsedProperty?.type == 'property' && parsedProperty.appliedModifiers != null) { + if (parsedProperty?.type == 'property' && parsedProperty.currentVariant.modifiers != null) { return { ...acc, - [propertyName]: parsedProperty.appliedModifiers, + [propertyName]: parsedProperty.currentVariant.modifiers, } } else { return acc diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts index 44b3e0c2e351..cf55d6460b90 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts @@ -19,7 +19,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 type { Config } from 'tailwindcss/types/config' -import { getPropertiesToAppliedModifiersMap } from './tailwind-media-query-utils' +import { getPropertiesToAppliedModifiersMap } from './tailwind-responsive-utils' export type ClassListUpdate = | { type: 'add'; property: string; value: string } diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 3eb1a137ee87..7a7691a3ffd0 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -4,12 +4,12 @@ import { defaultEither, Either, flatMapEither, isLeft } from '../../../core/shar import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { getElementFromProjectContents } from '../../editor/store/editor-state' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' -import { cssParsers, selectValueByBreakpoint } 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 { ParsedVariant, StyleInfo } from '../canvas-types' -import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types' +import { cssStyleProperty, cssVariant, type CSSStyleProperty } from '../canvas-types' import * as UCL from './tailwind-style-plugin-utils/update-class-list' import { assertNever } from '../../../core/shared/utils' import { @@ -20,7 +20,8 @@ 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 { MetadataUtils } from '../../../core/model/element-metadata-utils' -import { getModifiers } from './tailwind-style-plugin-utils/tailwind-media-query-utils' +import { getModifiers } from './tailwind-style-plugin-utils/tailwind-responsive-utils' +import { selectValueByBreakpoint } from '../responsive-utils' type StyleValueVariants = { value: string | number | undefined @@ -44,7 +45,7 @@ export const parseTailwindPropertyFactory = if (styleDefinition == null) { return null } - const parsed: ParsedVariant[] = styleDefinition + const possibleVariants: ParsedVariant[] = styleDefinition .map((v) => ({ parsedValue: cssParsers[prop](v.value, null), originalValue: v.value, @@ -58,18 +59,14 @@ export const parseTailwindPropertyFactory = >, })) - const result = selectValueByBreakpoint(parsed, context?.sceneWidth) - if (result == null) { + const selectedVariant = selectValueByBreakpoint(possibleVariants, context?.sceneWidth) + if (selectedVariant == null) { return null } return cssStyleProperty( - result.parsedValue, - jsExpressionValue(result.originalValue, emptyComments), - result.modifiers, - parsed.map((variant) => ({ - value: variant.parsedValue, - modifiers: variant.modifiers, - })), + jsExpressionValue(selectedVariant.originalValue, emptyComments), + cssVariant(selectedVariant.parsedValue, selectedVariant.modifiers), + possibleVariants.map((variant) => cssVariant(variant.parsedValue, variant.modifiers)), ) } diff --git a/editor/src/components/canvas/responsive-types.ts b/editor/src/components/canvas/responsive-types.ts new file mode 100644 index 000000000000..c1e025a6f8fc --- /dev/null +++ b/editor/src/components/canvas/responsive-types.ts @@ -0,0 +1,39 @@ +import type { Identifier, Dimension } from 'css-tree' +import type { CSSNumber } from '../inspector/common/css-utils' +// @media (min-width: 100px) and (max-width: 200em) => { min: { value: 100, unit: 'px' }, max: { value: 200, unit: 'em' } } +export type ScreenSize = { + min?: CSSNumber + max?: CSSNumber +} + +export interface MediaQuery { + type: 'MediaQuery' + loc: null + modifier: null + mediaType: null + condition?: { + type: 'Condition' + loc: null + kind: 'media' + children: Array + } +} + +export interface FeatureRange { + type: 'FeatureRange' + loc: null + kind: 'media' + left?: Dimension | Identifier + leftComparison: '<' | '>' + middle: Dimension | Identifier + rightComparison: '<' | '>' + right?: Dimension | Identifier +} + +export interface Feature { + type: 'Feature' + loc: null + kind: 'media' + name: 'min-width' | 'max-width' + value?: Dimension +} diff --git a/editor/src/components/canvas/responsive-utils.spec.ts b/editor/src/components/canvas/responsive-utils.spec.ts new file mode 100644 index 000000000000..a6c7f426a362 --- /dev/null +++ b/editor/src/components/canvas/responsive-utils.spec.ts @@ -0,0 +1,168 @@ +import * as csstree from 'css-tree' +import { mediaQueryToScreenSize, selectValueByBreakpoint } from './responsive-utils' +import type { ScreenSize, MediaQuery } from './responsive-types' +import { extractScreenSizeFromCss } from './responsive-utils' +import type { StyleMediaSizeModifier } from './canvas-types' + +describe('mediaQueryToScreenSize', () => { + it('converts simple screen size queries', () => { + const testCases: { input: string; expected: ScreenSize }[] = [ + { + input: '@media (100px 100px)', + expected: { min: { value: 100, unit: 'px' } }, + }, + ] + testCases.forEach((testCase) => { + csstree.walk(csstree.parse(testCase.input), (node) => { + if (node.type === 'MediaQuery') { + const result = mediaQueryToScreenSize(node as unknown as MediaQuery) + expect(result).toEqual(testCase.expected) + } + }) + }) + }) +}) + +describe('extractScreenSizeFromCss', () => { + beforeEach(() => { + // Clear the cache before each test + ;(extractScreenSizeFromCss as any).screenSizeCache?.clear() + }) + + it('extracts screen size from simple media query', () => { + const css = '@media (min-width: 100px) and (max-width: 500px)' + const result = extractScreenSizeFromCss(css) + expect(result).toEqual({ + min: { value: 100, unit: 'px' }, + max: { value: 500, unit: 'px' }, + }) + }) + + it('returns null for invalid media query', () => { + const css = 'not-a-media-query' + const result = extractScreenSizeFromCss(css) + expect(result).toBeNull() + }) + + it('uses cache for repeated calls with same CSS', () => { + const css = '@media (min-width: 100px)' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same object reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) // Use toBe for reference equality + expect(result1).toEqual({ + min: { value: 100, unit: 'px' }, + }) + }) + + it('caches null results', () => { + const css = 'invalid-css' + + // First call + const result1 = extractScreenSizeFromCss(css) + // Second call - should return same null reference + const result2 = extractScreenSizeFromCss(css) + + expect(result1).toBe(result2) + expect(result1).toBeNull() + }) + + it('handles different CSS strings independently in cache', () => { + const css1 = '@media (min-width: 100px)' + const css2 = '@media (max-width: 500px)' + + // First string + const result1a = extractScreenSizeFromCss(css1) + const result1b = extractScreenSizeFromCss(css1) + expect(result1a).toBe(result1b) + expect(result1a).toEqual({ + min: { value: 100, unit: 'px' }, + }) + + // Second string + const result2a = extractScreenSizeFromCss(css2) + const result2b = extractScreenSizeFromCss(css2) + expect(result2a).toBe(result2b) + expect(result2a).toEqual({ + max: { value: 500, unit: 'px' }, + }) + + // Different strings should have different references + expect(result1a).not.toBe(result2a) + }) +}) + +describe('selectValueByBreakpoint', () => { + const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ + { + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }, + { + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }, + { + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }, + { value: 'd' }, + ] + it('selects the correct value', () => { + expect(selectValueByBreakpoint(variants, 150)).toEqual({ + value: 'a', + modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], + }) + }) + it('select the closest value', () => { + expect(selectValueByBreakpoint(variants, 250)).toEqual({ + value: 'b', + modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], + }) + }) + it('converts em to px', () => { + expect(selectValueByBreakpoint(variants, 350)).toEqual({ + value: 'c', + modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], + }) + }) + it('selects the first value if no breakpoint is matched', () => { + expect(selectValueByBreakpoint(variants, 50)).toEqual({ value: 'd' }) + }) + it('selects null if no matching breakpoint and no default value', () => { + expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() + }) + it('selects default value if no media modifiers', () => { + expect( + selectValueByBreakpoint( + [ + { + value: 'a', + modifiers: [{ type: 'hover' }], + }, + { value: 'c' }, + ], + 50, + ), + ).toEqual({ value: 'c' }) + }) +}) diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts new file mode 100644 index 000000000000..799413d55a06 --- /dev/null +++ b/editor/src/components/canvas/responsive-utils.ts @@ -0,0 +1,227 @@ +import type { Feature, FeatureRange, MediaQuery, ScreenSize } from './responsive-types' +import * as csstree from 'css-tree' +import type { StyleMediaSizeModifier, StyleInfo, StyleModifier } from './canvas-types' +import { + type CSSNumber, + type CSSNumberUnit, + compValueAsPx, + cssNumber, +} from '../inspector/common/css-utils' + +function extractFromFeatureRange(featureRange: FeatureRange): { + leftValue: CSSNumber | null + rightValue: CSSNumber | null + leftComparison: '<' | '>' | null + rightComparison: '<' | '>' | null +} | null { + // 100px < width < 500px or 500px > width > 100px or 100px > width or 500px < width + if (featureRange?.middle?.type === 'Identifier' && featureRange.middle.name === 'width') { + const leftValue = + featureRange.left?.type === 'Dimension' + ? cssNumber(Number(featureRange.left.value), featureRange.left.unit as CSSNumberUnit) + : null + + const rightValue = + featureRange.right?.type === 'Dimension' + ? cssNumber(Number(featureRange.right.value), featureRange.right.unit as CSSNumberUnit) + : null + + return { + leftValue: leftValue, + rightValue: rightValue, + leftComparison: featureRange.leftComparison, + rightComparison: featureRange.rightComparison, + } + } + // width > 100px or width < 500px + if (featureRange?.left?.type === 'Identifier' && featureRange.left.name === 'width') { + const rightValue = + featureRange.middle?.type === 'Dimension' + ? cssNumber(Number(featureRange.middle.value), featureRange.middle.unit as CSSNumberUnit) + : null + // this is not a mistake, since we normalize the "width" to be in the middle + const rightComparison = featureRange.leftComparison + + return { + leftValue: null, + leftComparison: null, + rightValue: rightValue, + rightComparison: rightComparison, + } + } + return null +} +export function mediaQueryToScreenSize(mediaQuery: MediaQuery): ScreenSize { + const result: ScreenSize = {} + + if (mediaQuery.condition?.type === 'Condition') { + // Handle FeatureRange case + const featureRanges = mediaQuery.condition.children.filter( + (child): child is FeatureRange => child.type === 'FeatureRange', + ) as Array + + featureRanges.forEach((featureRange) => { + const rangeData = extractFromFeatureRange(featureRange) + if (rangeData == null) { + return + } + const { leftValue, rightValue, leftComparison, rightComparison } = rangeData + if (leftValue != null) { + if (leftComparison === '<') { + result.min = leftValue + } else { + result.max = leftValue + } + } + if (rightValue != null) { + if (rightComparison === '<') { + result.max = rightValue + } else { + result.min = rightValue + } + } + }) + + // Handle Feature case (min-width/max-width) + const features = mediaQuery.condition.children.filter( + (child): child is Feature => child.type === 'Feature', + ) + features.forEach((feature) => { + if (feature.value?.type === 'Dimension') { + if (feature.name === 'min-width') { + result.min = cssNumber(Number(feature.value.value), feature.value.unit as CSSNumberUnit) + } else if (feature.name === 'max-width') { + result.max = cssNumber(Number(feature.value.value), feature.value.unit as CSSNumberUnit) + } + } + }) + } + + return result +} + +export function extractMediaQueryFromCss(css: string): MediaQuery | null { + let result: MediaQuery | null = null + csstree.walk(csstree.parse(css), (node) => { + if (node.type === 'MediaQuery') { + result = node as unknown as MediaQuery + } + }) + return result +} + +// Cache for storing previously parsed screen sizes +const screenSizeCache: Map = new Map() +export function extractScreenSizeFromCss(css: string): ScreenSize | null { + // Check cache first + const cached = screenSizeCache.get(css) + if (cached !== undefined) { + return cached + } + + // If not in cache, compute and store result + const mediaQuery = extractMediaQueryFromCss(css) + const result = mediaQuery == null ? null : mediaQueryToScreenSize(mediaQuery) + + screenSizeCache.set(css, result) + return result +} + +function getMediaModifier( + modifiers: StyleModifier[] | undefined | null, +): StyleMediaSizeModifier | null { + return (modifiers ?? []).filter( + (modifier): modifier is StyleMediaSizeModifier => modifier.type === 'media-size', + )[0] +} + +export function selectValueByBreakpoint( + parsedVariants: T[], + sceneWidthInPx?: number, +): T | null { + const relevantModifiers = parsedVariants.filter((variant) => { + // 1. filter out variants that don't have media modifiers, but keep variants with no modifiers at all + if (variant.modifiers == null || variant.modifiers.length === 0) { + return true + } + // FIXME - we take only the first media modifier for now + const mediaModifier = getMediaModifier(variant.modifiers) + if (mediaModifier == null) { + // this means it only has other modifiers + return false + } + + if (sceneWidthInPx == null) { + // filter out variants that require a scene width + return false + } + + // 2. check that it has at least one media modifier that satisfies the current scene width + const maxSizeInPx = compValueAsPx(mediaModifier.size.max) + const minSizeInPx = compValueAsPx(mediaModifier.size.min) + + // if it has only max + if (maxSizeInPx != null && minSizeInPx == null && sceneWidthInPx <= maxSizeInPx) { + return true + } + + // if it has only min + if (maxSizeInPx == null && minSizeInPx != null && sceneWidthInPx >= minSizeInPx) { + return true + } + + // if it has both max and min + if ( + maxSizeInPx != null && + minSizeInPx != null && + sceneWidthInPx >= minSizeInPx && + sceneWidthInPx <= maxSizeInPx + ) { + return true + } + return false + }) + let chosen: T | null = null + for (const variant of relevantModifiers) { + const chosenMediaModifier = getMediaModifier(chosen?.modifiers) + const variantMediaModifier = getMediaModifier(variant.modifiers) + if (variantMediaModifier == null) { + if (chosenMediaModifier == null) { + // if we have nothing chosen then we'll take the base value + chosen = variant + } + continue + } + if (chosenMediaModifier == null) { + chosen = variant + continue + } + // fixme - select by actual media query precedence + const minSizeInPx = compValueAsPx(variantMediaModifier.size.min) + const chosenMinSizeInPx = compValueAsPx(chosenMediaModifier.size.min) + if (minSizeInPx != null && (chosenMinSizeInPx == null || minSizeInPx > chosenMinSizeInPx)) { + chosen = variant + } + const maxSizeInPx = compValueAsPx(variantMediaModifier.size.max) + const chosenMaxSizeInPx = compValueAsPx(chosenMediaModifier.size.max) + if (maxSizeInPx != null && (chosenMaxSizeInPx == null || maxSizeInPx < chosenMaxSizeInPx)) { + chosen = variant + } + } + if (chosen == null) { + return null + } + return chosen +} + +export function getAppliedMediaSizeModifierFromBreakpoint( + styleInfo: StyleInfo, + prop: keyof StyleInfo, +): StyleMediaSizeModifier | null { + if (styleInfo == null) { + return null + } + return styleInfo[prop]?.type === 'property' + ? (styleInfo[prop].currentVariant.modifiers ?? []).find((m) => m.type === 'media-size') ?? null + : null +} diff --git a/editor/src/components/inspector/common/css-utils.spec.ts b/editor/src/components/inspector/common/css-utils.spec.ts index 132561c71912..d523ea68d44f 100644 --- a/editor/src/components/inspector/common/css-utils.spec.ts +++ b/editor/src/components/inspector/common/css-utils.spec.ts @@ -81,7 +81,6 @@ import { printBackgroundSize, printGridDimensionCSS, RegExpLibrary, - selectValueByBreakpoint, stringifyGridDimension, toggleSimple, toggleStylePropPath, @@ -2137,59 +2136,3 @@ describe('parseGridRange', () => { expect(got).toEqual(right(gridRange(gridSpanArea('some-area'), gridPositionValue(2)))) }) }) - -describe('selectValueByBreakpoint', () => { - const variants: { value: string; modifiers?: StyleMediaSizeModifier[] }[] = [ - { - value: 'b', - modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], - }, - { - value: 'a', - modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], - }, - { - value: 'c', - modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], - }, - { value: 'd' }, - ] - it('selects the correct value', () => { - expect(selectValueByBreakpoint(variants, 150)).toEqual({ - value: 'a', - modifiers: [{ type: 'media-size', size: { min: { value: 100, unit: 'px' } } }], - }) - }) - it('select the closest value', () => { - expect(selectValueByBreakpoint(variants, 250)).toEqual({ - value: 'b', - modifiers: [{ type: 'media-size', size: { min: { value: 200, unit: 'px' } } }], - }) - }) - it('converts em to px', () => { - expect(selectValueByBreakpoint(variants, 350)).toEqual({ - value: 'c', - modifiers: [{ type: 'media-size', size: { min: { value: 20, unit: 'em' } } }], - }) - }) - it('selects the first value if no breakpoint is matched', () => { - expect(selectValueByBreakpoint(variants, 50)).toEqual({ value: 'd' }) - }) - it('selects null if no matching breakpoint and no default value', () => { - expect(selectValueByBreakpoint(variants.slice(0, 2), 50)).toBeNull() - }) - it('selects default value if no media modifiers', () => { - expect( - selectValueByBreakpoint( - [ - { - value: 'a', - modifiers: [{ type: 'hover' }], - }, - { value: 'c' }, - ], - 50, - ), - ).toEqual({ value: 'c' }) - }) -}) diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index f3b158e0f0c4..139caa4e84ac 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -86,8 +86,6 @@ import { memoize } from '../../../core/shared/memoize' import * as csstree from 'css-tree' import type { IcnProps } from '../../../uuiui' import { cssNumberEqual } from '../../canvas/controls/select-mode/controls-common' -import type { StyleInfo } from '../../canvas/canvas-types' -import type { CompValue, StyleMediaSizeModifier, StyleModifier } from '../../canvas/canvas-types' var combineRegExp = function (regexpList: Array, flags?: string) { let source: string = '' @@ -5984,105 +5982,6 @@ export function maybeParseGridLine( } const EM_TO_PX_RATIO = 16 -function compValueAsPx(value: CompValue | null | undefined): number | null { +export function compValueAsPx(value: CSSNumber | null | undefined): number | null { return value == null ? null : value.unit === 'em' ? value.value * EM_TO_PX_RATIO : value.value } - -function getMediaModifier( - modifiers: StyleModifier[] | undefined | null, -): StyleMediaSizeModifier | null { - return (modifiers ?? []).filter( - (modifier): modifier is StyleMediaSizeModifier => modifier.type === 'media-size', - )[0] -} - -export function selectValueByBreakpoint( - parsedVariants: T[], - sceneWidthInPx?: number, -): T | null { - const relevantModifiers = parsedVariants.filter((variant) => { - // 1. filter out variants that don't have media modifiers, but keep variants with no modifiers at all - if (variant.modifiers == null || variant.modifiers.length === 0) { - return true - } - // FIXME - we take only the first media modifier for now - const mediaModifier = getMediaModifier(variant.modifiers) - if (mediaModifier == null) { - // this means it only has other modifiers - return false - } - - if (sceneWidthInPx == null) { - // filter out variants that require a scene width - return false - } - - // 2. check that it has at least one media modifier that satisfies the current scene width - const maxSizeInPx = compValueAsPx(mediaModifier.size.max) - const minSizeInPx = compValueAsPx(mediaModifier.size.min) - - // if it has only max - if (maxSizeInPx != null && minSizeInPx == null && sceneWidthInPx <= maxSizeInPx) { - return true - } - - // if it has only min - if (maxSizeInPx == null && minSizeInPx != null && sceneWidthInPx >= minSizeInPx) { - return true - } - - // if it has both max and min - if ( - maxSizeInPx != null && - minSizeInPx != null && - sceneWidthInPx >= minSizeInPx && - sceneWidthInPx <= maxSizeInPx - ) { - return true - } - return false - }) - let chosen: T | null = null - for (const variant of relevantModifiers) { - const chosenMediaModifier = getMediaModifier(chosen?.modifiers) - const variantMediaModifier = getMediaModifier(variant.modifiers) - if (variantMediaModifier == null) { - if (chosenMediaModifier == null) { - // if we have nothing chosen then we'll take the base value - chosen = variant - } - continue - } - if (chosenMediaModifier == null) { - chosen = variant - continue - } - // fixme - select by actual media query precedence - const minSizeInPx = compValueAsPx(variantMediaModifier.size.min) - const chosenMinSizeInPx = compValueAsPx(chosenMediaModifier.size.min) - if (minSizeInPx != null && (chosenMinSizeInPx == null || minSizeInPx > chosenMinSizeInPx)) { - chosen = variant - } - const maxSizeInPx = compValueAsPx(variantMediaModifier.size.max) - const chosenMaxSizeInPx = compValueAsPx(chosenMediaModifier.size.max) - if (maxSizeInPx != null && (chosenMaxSizeInPx == null || maxSizeInPx < chosenMaxSizeInPx)) { - chosen = variant - } - } - if (chosen == null) { - return null - } - return chosen -} - -export function getAppliedMediaSizeModifierFromBreakpoint( - styleInfo: StyleInfo, - prop: keyof StyleInfo, -): StyleMediaSizeModifier | null { - if (styleInfo == null) { - return null - } - return styleInfo[prop]?.type === 'property' - ? (styleInfo[prop].appliedModifiers ?? []).find((m) => m.type === 'media-size') ?? null - : null -} diff --git a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx index 411211ef8f96..501d9da01d84 100644 --- a/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx +++ b/editor/src/components/inspector/sections/style-section/container-subsection/padding-row.tsx @@ -50,8 +50,8 @@ import { } from '../../layout-section/layout-system-subsection/split-chained-number-input' import { getActivePlugin } from '../../../../canvas/plugins/style-plugins' import type { StyleInfo } from '../../../../canvas/canvas-types' -import { getAppliedMediaSizeModifierFromBreakpoint } from '../../../common/css-utils' -import { getTailwindVariantFromAppliedModifier } from '../../../../canvas/plugins/tailwind-style-plugin-utils/tailwind-media-query-utils' +import { getTailwindVariantFromAppliedModifier } from '../../../../canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils' +import { getAppliedMediaSizeModifierFromBreakpoint } from '../../../../canvas/responsive-utils' function buildPaddingPropsToUnset(propertyTarget: ReadonlyArray): Array { return [ From 298e9dd032e3dc19b15887c5d507da9e93239aac Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:12:37 +0200 Subject: [PATCH 17/21] cleanup --- editor/src/components/inspector/common/css-utils.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/components/inspector/common/css-utils.spec.ts b/editor/src/components/inspector/common/css-utils.spec.ts index d523ea68d44f..b27f1c8d8303 100644 --- a/editor/src/components/inspector/common/css-utils.spec.ts +++ b/editor/src/components/inspector/common/css-utils.spec.ts @@ -85,7 +85,6 @@ import { toggleSimple, toggleStylePropPath, } from './css-utils' -import type { StyleMediaSizeModifier } from '../../canvas/canvas-types' describe('toggleStyleProp', () => { const simpleToggleProp = toggleStylePropPath(PP.create('style', 'backgroundColor'), toggleSimple) From 2cdfff21f4788a25fbf82dcb9217c90dbbd47a22 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:20:32 +0200 Subject: [PATCH 18/21] organize types --- .../canvas-strategies/canvas-strategy-types.ts | 6 ++++-- .../src/components/canvas/plugins/style-plugins.ts | 7 ++++--- .../tailwind-responsive-utils.ts | 5 ++--- .../update-class-list.ts | 5 ++--- .../canvas/plugins/tailwind-style-plugin.ts | 13 +++---------- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts index 0e98222ddd13..03da8982132a 100644 --- a/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts +++ b/editor/src/components/canvas/canvas-strategies/canvas-strategy-types.ts @@ -112,10 +112,12 @@ export function controlWithProps

(value: ControlWithProps

): ControlWithProp export type StyleInfoReader = (elementPath: ElementPath) => StyleInfo | null -export type StyleInfoFactory = (context: { +export type StyleInfoContext = { projectContents: ProjectContentTreeRoot jsxMetadata: ElementInstanceMetadataMap -}) => StyleInfoReader +} + +export type StyleInfoFactory = (context: StyleInfoContext) => StyleInfoReader export interface InteractionCanvasState { interactionTarget: InteractionTarget diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index 206f63d2daff..e3569302dc9a 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -51,15 +51,16 @@ export function deleteCSSProp(property: string): DeleteCSSProp { export type StyleUpdate = UpdateCSSProp | DeleteCSSProp +export type StylePluginContext = { + sceneWidth?: number +} export interface StylePlugin { name: string styleInfoFactory: StyleInfoFactory readStyleFromElementProps: ( attributes: JSXAttributes, prop: T, - context?: { - sceneWidth?: number - }, + context: StylePluginContext, ) => CSSStyleProperty> | null updateStyles: ( editorState: EditorState, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts index 7cda77cbb34f..3e0f6ca423c6 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.ts @@ -5,6 +5,7 @@ import { extractScreenSizeFromCss } from '../../responsive-utils' import { TailwindPropertyMapping } from '../tailwind-style-plugin' import { parseTailwindPropertyFactory } from '../tailwind-style-plugin' import { getTailwindClassMapping } from '../tailwind-style-plugin' +import type { StylePluginContext } from '../style-plugins' export const TAILWIND_DEFAULT_SCREENS = { sm: '640px', @@ -82,9 +83,7 @@ export function getPropertiesToAppliedModifiersMap( currentClassNameAttribute: string, propertyNames: string[], config: Config | null, - context: { - sceneWidth?: number - }, + context: StylePluginContext, ): Record { const parseTailwindProperty = parseTailwindPropertyFactory(config, context) const classMapping = getTailwindClassMapping(currentClassNameAttribute.split(' '), config) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts index cf55d6460b90..e22d61796e89 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/update-class-list.ts @@ -20,6 +20,7 @@ import { applyValuesAtPath } from '../../commands/utils/property-utils' import * as PP from '../../../../core/shared/property-path' import type { Config } from 'tailwindcss/types/config' import { getPropertiesToAppliedModifiersMap } from './tailwind-responsive-utils' +import type { StylePluginContext } from '../style-plugins' export type ClassListUpdate = | { type: 'add'; property: string; value: string } @@ -40,9 +41,7 @@ export const runUpdateClassList = ( element: ElementPath, classNameUpdates: ClassListUpdate[], config: Config | null, - context: { - sceneWidth?: number - }, + context: StylePluginContext, ): EditorStateWithPatch => { const currentClassNameAttribute = getClassNameAttribute(getElementFromProjectContents(element, editorState.projectContents)) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 7a7691a3ffd0..9eee2e85972c 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -6,7 +6,7 @@ import { getElementFromProjectContents } 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 { StylePlugin, StylePluginContext } from './style-plugins' import type { Config } from 'tailwindcss/types/config' import type { ParsedVariant, StyleInfo } from '../canvas-types' import { cssStyleProperty, cssVariant, type CSSStyleProperty } from '../canvas-types' @@ -32,12 +32,7 @@ export type TailwindHoverModifier = { type: 'hover'; value: string } export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier export const parseTailwindPropertyFactory = - ( - config: Config | null, - context: { - sceneWidth?: number - }, - ) => + (config: Config | null, context: StylePluginContext) => ( styleDefinition: StyleValueVariants | undefined, prop: T, @@ -173,9 +168,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { readStyleFromElementProps:

( attributes: JSXAttributes, prop: P, - context?: { - sceneWidth?: number - }, + context: StylePluginContext, ): CSSStyleProperty> | null => { const classNameAttribute = defaultEither( null, From 4602e9b7d2091e4dca605d5279b0c338bbe7b21a Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:21:27 +0200 Subject: [PATCH 19/21] organize types #2 --- .../tailwind-responsive-utils.spec.ts | 2 +- .../src/components/canvas/plugins/tailwind-style-plugin.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.spec.ts index 130c65d0e92c..2787e8ecb8e7 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin-utils/tailwind-responsive-utils.spec.ts @@ -1,4 +1,4 @@ -import type { Config } from 'tailwindcss' +import type { Config } from 'tailwindcss/types/config' import { getModifiers, screensConfigToScreenSizes } from './tailwind-responsive-utils' describe('getModifiers', () => { diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts index 2919bf9eb5c9..cf1e604a3833 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts @@ -9,7 +9,6 @@ import { TailwindPlugin } from './tailwind-style-plugin' import { createModifiedProject } from '../../../sample-projects/sample-project-utils.test-utils' import { TailwindConfigPath } from '../../../core/tailwind/tailwind-config' import { getTailwindConfigCached } from '../../../core/tailwind/tailwind-compilation' -import type { Config } from 'tailwindcss/types/config' const Project = createModifiedProject({ [StoryboardFilePath]: ` From 67cdda59096021818e92bd83ec30695ea63ce26a Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:33:39 +0200 Subject: [PATCH 20/21] refactor scene width getter --- .../canvas/plugins/tailwind-style-plugin.ts | 13 +++---------- editor/src/components/canvas/responsive-utils.ts | 11 +++++++++++ .../inspector/common/property-path-hooks.ts | 14 ++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 9eee2e85972c..9eb52679d812 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -19,9 +19,8 @@ import { 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 { MetadataUtils } from '../../../core/model/element-metadata-utils' import { getModifiers } from './tailwind-style-plugin-utils/tailwind-responsive-utils' -import { selectValueByBreakpoint } from '../responsive-utils' +import { getContainingSceneWidth, selectValueByBreakpoint } from '../responsive-utils' type StyleValueVariants = { value: string | number | undefined @@ -198,9 +197,8 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { } const mapping = getTailwindClassMapping(classList.split(' '), config) - const containingScene = MetadataUtils.getParentSceneMetadata(jsxMetadata, elementPath) const parseTailwindProperty = parseTailwindPropertyFactory(config, { - sceneWidth: containingScene?.specialSizeMeasurements?.clientWidth, + sceneWidth: getContainingSceneWidth(elementPath, jsxMetadata), }) return { gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], 'gap'), @@ -261,11 +259,6 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { } }, updateStyles: (editorState, elementPath, updates) => { - const containingScene = MetadataUtils.getParentSceneMetadata( - editorState.jsxMetadata, - elementPath, - ) - const sceneWidth = containingScene?.specialSizeMeasurements?.clientWidth const propsToDelete = mapDropNulls( (update) => update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe @@ -289,7 +282,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { elementPath, [...propsToDelete, ...propsToSet], config, - { sceneWidth: sceneWidth }, + { sceneWidth: getContainingSceneWidth(elementPath, editorState.jsxMetadata) }, ) }, } diff --git a/editor/src/components/canvas/responsive-utils.ts b/editor/src/components/canvas/responsive-utils.ts index 799413d55a06..dcaf0232b55a 100644 --- a/editor/src/components/canvas/responsive-utils.ts +++ b/editor/src/components/canvas/responsive-utils.ts @@ -7,6 +7,9 @@ import { compValueAsPx, cssNumber, } from '../inspector/common/css-utils' +import type { ElementPath } from 'utopia-shared/src/types' +import type { ElementInstanceMetadataMap } from '../../core/shared/element-template' +import { MetadataUtils } from '../../core/model/element-metadata-utils' function extractFromFeatureRange(featureRange: FeatureRange): { leftValue: CSSNumber | null @@ -214,6 +217,14 @@ export function selectValueByBreakpoint @@ -764,14 +764,12 @@ export function useGetMultiselectedProps

( const styleInfoReaderRef = useRefEditorState( (store) => (props: JSXAttributes, prop: keyof StyleInfo): GetModifiableAttributeResult => { - // todo: support multiple selected elements - const selectedElement = store.editor.selectedViews[0] - const sceneMetadata = MetadataUtils.getParentSceneMetadata( - store.editor.jsxMetadata, - selectedElement, - ) const elementStyle = getActivePlugin(store.editor).readStyleFromElementProps(props, prop, { - sceneWidth: sceneMetadata?.specialSizeMeasurements?.clientWidth, + sceneWidth: getContainingSceneWidth( + // todo: support multiple selected elements + store.editor.selectedViews[0], + store.editor.jsxMetadata, + ), }) if (elementStyle == null) { return right({ type: 'ATTRIBUTE_NOT_FOUND' }) From de74fedb4a1a70eebcd6486525fde9bed768a192 Mon Sep 17 00:00:00 2001 From: Liad Yosef Date: Mon, 9 Dec 2024 23:47:58 +0200 Subject: [PATCH 21/21] reorder --- .../canvas/plugins/tailwind-style-plugin.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 9eb52679d812..73926a6df99d 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -24,11 +24,8 @@ import { getContainingSceneWidth, selectValueByBreakpoint } from '../responsive- type StyleValueVariants = { value: string | number | undefined - modifiers?: TailwindGeneralModifier[] + modifiers?: TailwindModifier[] }[] -export type TailwindMediaModifier = { type: 'media'; value: string } -export type TailwindHoverModifier = { type: 'hover'; value: string } -export type TailwindGeneralModifier = TailwindMediaModifier | TailwindHoverModifier export const parseTailwindPropertyFactory = (config: Config | null, context: StylePluginContext) => @@ -138,9 +135,7 @@ export function getTailwindClassMapping( return } mapping[parsed.property] = mapping[parsed.property] ?? [] - const modifiers = (parsed.variants ?? []).filter( - (v): v is TailwindGeneralModifier => v.type === 'media' || v.type === 'hover', - ) + const modifiers = (parsed.variants ?? []).filter(isTailwindModifier) mapping[parsed.property].push({ value: parsed.value, modifiers: modifiers, @@ -287,3 +282,11 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => { }, } } + +type TailwindModifier = { type: 'media'; value: string } | { type: 'hover'; value: string } +function isTailwindModifier(modifier: { + type: string + value: string +}): modifier is TailwindModifier { + return modifier.type === 'media' || modifier.type === 'hover' +}