From 4bef7db95f0bf98e85885524152523609b4a3e45 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 20 Feb 2025 13:07:21 +0000 Subject: [PATCH 01/15] working version with details/summary --- .../sections/advanced/advanced.tsx | 230 ++++++++++++------ .../style-panel/sections/advanced/stores.ts | 25 +- packages/css-engine/src/core/index.ts | 2 +- packages/css-engine/src/core/merger.ts | 25 +- 4 files changed, 197 insertions(+), 85 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 49035eca878f..cda4bfa14cd4 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -15,6 +15,7 @@ import { matchSorter } from "match-sorter"; import { PlusIcon } from "@webstudio-is/icons"; import { Box, + Collapsible, Flex, Label, SearchField, @@ -29,6 +30,8 @@ import { import { propertyDescriptions } from "@webstudio-is/css-data"; import { hyphenateProperty, + isShorthand, + StyleValue, toValue, type StyleProperty, } from "@webstudio-is/css-engine"; @@ -255,7 +258,64 @@ const AdvancedPropertyValue = ({ * To fix this, we skip rendering properties not visible in the viewport using the contentvisibilityautostatechange event, * and the contentVisibility and containIntrinsicSize CSS properties. */ -const AdvancedProperty = memo( +const LazyRender = ({ children }: ComponentProps<"div">) => { + const visibilityChangeEventSupported = useClientSupports( + () => "oncontentvisibilityautostatechange" in document.body + ); + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(!visibilityChangeEventSupported); + + useEffect(() => { + if (!visibilityChangeEventSupported) { + return; + } + + if (ref.current == null) { + return; + } + + const controller = new AbortController(); + + ref.current.addEventListener( + "contentvisibilityautostatechange", + (event) => { + console.log("visibility", event); + setIsVisible(!event.skipped); + }, + { + signal: controller.signal, + } + ); + + return () => { + controller.abort(); + }; + }, [visibilityChangeEventSupported]); + + return ( +
+ { + //children + isVisible ? children : undefined + } +
+ ); +}; + +const AdvancedDeclarationLonghand = memo( ({ property, autoFocus, @@ -264,6 +324,7 @@ const AdvancedProperty = memo( valueInputRef, }: { property: StyleProperty; + value: StyleValue; autoFocus?: boolean; onReset?: () => void; onChangeComplete?: ComponentProps< @@ -271,60 +332,59 @@ const AdvancedProperty = memo( >["onChangeComplete"]; valueInputRef?: RefObject; }) => { - const visibilityChangeEventSupported = useClientSupports( - () => "oncontentvisibilityautostatechange" in document.body - ); - const ref = useRef(null); - const [isVisible, setIsVisible] = useState(!visibilityChangeEventSupported); - - useEffect(() => { - if (!visibilityChangeEventSupported) { - return; - } - - if (ref.current == null) { - return; - } - - const controller = new AbortController(); - - ref.current.addEventListener( - "contentvisibilityautostatechange", - (event) => { - setIsVisible(!event.skipped); - }, - { - signal: controller.signal, - } - ); - - return () => { - controller.abort(); - }; - }, [visibilityChangeEventSupported]); - return ( - {isVisible && ( - <> + + + : + + + + ); + } +); + +const AdvancedDeclarationShorthand = memo( + (props: { + property: StyleProperty; + value: StyleValue; + autoFocus?: boolean; + onReset?: () => void; + onChangeComplete?: ComponentProps< + typeof CssValueInputContainer + >["onChangeComplete"]; + valueInputRef?: RefObject; + }) => { + const { property, value, onReset } = props; + return ( +
+ + : - - - - - )} - + {toValue(value)} + + + +
); } ); @@ -472,25 +525,30 @@ export const Section = () => { {showRecentProperties && recentProperties.map((property, index, properties) => { const isLast = index === properties.length - 1; + const AdvancedDeclaration = isShorthand(property) + ? AdvancedDeclarationShorthand + : AdvancedDeclarationLonghand; return ( - { - if (event.type === "enter") { - handleShowAddStyleInput(); - } - }} - onReset={() => { - updateRecentProperties( - recentProperties.filter( - (recentProperty) => recentProperty !== property - ) - ); - }} - /> + + { + if (event.type === "enter") { + handleShowAddStyleInput(); + } + }} + onReset={() => { + updateRecentProperties( + recentProperties.filter( + (recentProperty) => recentProperty !== property + ) + ); + }} + /> + ); })} {(showRecentProperties || isAdding) && ( @@ -534,9 +592,19 @@ export const Section = () => { .filter( (property) => recentProperties.includes(property) === false ) - .map((property) => ( - - ))} + .map((property) => { + const AdvancedDeclaration = isShorthand(property) + ? AdvancedDeclarationShorthand + : AdvancedDeclarationLonghand; + return ( + + + + ); + })} diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 6df362505f6f..29d93e628280 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -1,5 +1,10 @@ import { computed } from "nanostores"; -import { type StyleMap, type StyleProperty } from "@webstudio-is/css-engine"; +import { + hyphenateProperty, + mergeStyles, + type StyleMap, + type StyleProperty, +} from "@webstudio-is/css-engine"; import { $matchingBreakpoints, getDefinedStyles } from "../../shared/model"; import { sections } from "../sections"; import { @@ -9,6 +14,7 @@ import { } from "~/shared/nano-states"; import { $selectedInstancePath } from "~/shared/awareness"; import { $settings } from "~/builder/shared/client-settings"; +import { camelCase } from "change-case"; const initialProperties = new Set([ "cursor", @@ -77,6 +83,21 @@ export const $advancedStyles = computed( advancedStyles.set(initialProperty, { type: "unset", value: "" }); } } - return advancedStyles; + //console.log(advancedStyles); + const styles1: StyleMap = new Map(); + + for (const [property, value] of advancedStyles) { + styles1.set(hyphenateProperty(property), value); + } + + const merged = mergeStyles(styles1); + + const styles2: StyleMap = new Map(); + + for (const [property, value] of merged) { + styles2.set(camelCase(property), value); + } + console.log(styles2); + return styles2; } ); diff --git a/packages/css-engine/src/core/index.ts b/packages/css-engine/src/core/index.ts index 222d4023ff02..acbc45904a2a 100644 --- a/packages/css-engine/src/core/index.ts +++ b/packages/css-engine/src/core/index.ts @@ -6,7 +6,7 @@ export type { FontFaceRule, } from "./rules"; export { prefixStyles } from "./prefixer"; -export { mergeStyles } from "./merger"; +export { mergeStyles, isShorthand } from "./merger"; export { generateStyleMap } from "./rules"; export type { StyleSheetRegular } from "./style-sheet-regular"; export * from "./create-style-sheet"; diff --git a/packages/css-engine/src/core/merger.ts b/packages/css-engine/src/core/merger.ts index bb470d7e812e..9e86c58f193c 100644 --- a/packages/css-engine/src/core/merger.ts +++ b/packages/css-engine/src/core/merger.ts @@ -1,7 +1,13 @@ -import { StyleValue, TupleValue, TupleValueItem } from "../schema"; +import { + StyleValue, + TupleValue, + TupleValueItem, + type StyleProperty, +} from "../schema"; import { cssWideKeywords } from "../css"; import type { StyleMap } from "./rules"; import { toValue } from "./to-value"; +import { hyphenateProperty } from "./to-property"; /** * Css wide keywords cannot be used in shorthand parts @@ -138,6 +144,23 @@ const mergeBackgroundPosition = (styleMap: StyleMap) => { } }; +const supportedShorthandProperties = new Set([ + "margin", + "padding", + "border", + "outline", + "border-top", + "border-right", + "border-bottom", + "border-left", + "white-space", + "text-wrap", +]); + +export const isShorthand = (property: string) => { + return supportedShorthandProperties.has(hyphenateProperty(property)); +}; + export const mergeStyles = (styleMap: StyleMap) => { const newStyle = new Map(styleMap); mergeBorder(newStyle, "border-top"); From 010609ba83b8c3f71f801aac51e7cceedcc7da7d Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 21 Feb 2025 19:51:37 +0000 Subject: [PATCH 02/15] wip --- .../sections/advanced/advanced.tsx | 66 ++++++++++++++----- .../sections/advanced/copy-paste-menu.tsx | 23 ++++--- .../style-panel/sections/advanced/stores.ts | 29 ++++---- .../builder/shared/collapsible-section.tsx | 48 +++++++------- 4 files changed, 104 insertions(+), 62 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index cda4bfa14cd4..13926c53d0b0 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -17,6 +17,7 @@ import { Box, Collapsible, Flex, + focusRingStyle, Label, SearchField, SectionTitle, @@ -27,7 +28,11 @@ import { theme, Tooltip, } from "@webstudio-is/design-system"; -import { propertyDescriptions } from "@webstudio-is/css-data"; +import { + expandShorthands, + parseCssValue, + propertyDescriptions, +} from "@webstudio-is/css-data"; import { hyphenateProperty, isShorthand, @@ -55,12 +60,13 @@ import { getDots } from "../../shared/style-section"; import { PropertyInfo } from "../../property-label"; import { ColorPopover } from "../../shared/color-picker"; import { useClientSupports } from "~/shared/client-supports"; -import { CopyPasteMenu, propertyContainerAttribute } from "./copy-paste-menu"; -import { $advancedStyles } from "./stores"; +import { CopyPasteMenu, copyAttribute } from "./copy-paste-menu"; +import { $advancedStylesShorthands } from "./stores"; import { $settings } from "~/builder/shared/client-settings"; import { AddStyleInput } from "./add-style-input"; import { parseStyleInput } from "./parse-style-input"; import { $selectedInstanceKey } from "~/shared/awareness"; +import { camelCase } from "change-case"; // Only here to keep the same section module interface export const properties = []; @@ -117,7 +123,7 @@ const insertStyles = (css: string) => { // Used to indent the values when they are on the next line. This way its easier to see // where the property ends and value begins, especially in case of presets. -const indentation = `20px`; +const initialIndentation = `20px`; const AdvancedPropertyLabel = ({ property, @@ -166,7 +172,7 @@ const AdvancedPropertyLabel = ({ text="mono" css={{ backgroundColor: "transparent", - marginLeft: `-${indentation}`, + marginLeft: `-${initialIndentation}`, }} > {label} @@ -279,7 +285,6 @@ const LazyRender = ({ children }: ComponentProps<"div">) => { ref.current.addEventListener( "contentvisibilityautostatechange", (event) => { - console.log("visibility", event); setIsVisible(!event.skipped); }, { @@ -322,10 +327,12 @@ const AdvancedDeclarationLonghand = memo( onChangeComplete, onReset, valueInputRef, + indentation = initialIndentation, }: { property: StyleProperty; - value: StyleValue; + value: StyleValue | undefined; autoFocus?: boolean; + indentation?: string; onReset?: () => void; onChangeComplete?: ComponentProps< typeof CssValueInputContainer @@ -338,7 +345,7 @@ const AdvancedDeclarationLonghand = memo( wrap="wrap" align="center" justify="start" - {...{ [propertyContainerAttribute]: property }} + {...{ [copyAttribute]: property }} > void; onChangeComplete?: ComponentProps< @@ -375,15 +382,24 @@ const AdvancedDeclarationShorthand = memo( valueInputRef?: RefObject; }) => { const { property, value, onReset } = props; + const [isOpen, setIsOpen] = useState(false); + const longhands = expandShorthands([[property, toValue(value)]]); + return ( -
- + + - : + {isOpen ? ": ▼" : ": ▶"} {toValue(value)} - - -
+ + + + {longhands.map(([property, value]) => { + return ( + + ); + })} + + ); } ); export const Section = () => { const [isAdding, setIsAdding] = useState(false); - const advancedStyles = useStore($advancedStyles); + const advancedStyles = useStore($advancedStylesShorthands); const selectedInstanceKey = useStore($selectedInstanceKey); // Memorizing recent properties by instance, so that when user switches between instances and comes back // they are still in-place diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx index 3c27bfd521ff..94f875c3d578 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx @@ -14,9 +14,9 @@ import { type StyleMap, } from "@webstudio-is/css-engine"; import { useStore } from "@nanostores/react"; -import { $advancedStyles } from "./stores"; +import { $advancedStylesShorthands, $advancedStylesLonghands } from "./stores"; -export const propertyContainerAttribute = "data-property"; +export const copyAttribute = "data-declaration"; export const CopyPasteMenu = ({ children, @@ -27,7 +27,8 @@ export const CopyPasteMenu = ({ properties: Array; onPaste: (cssText: string) => void; }) => { - const advancedStyles = useStore($advancedStyles); + const advancedStylesShorthands = useStore($advancedStylesShorthands); + const advancedStylesLonghands = useStore($advancedStylesLonghands); const lastClickedProperty = useRef(); const handlePaste = () => { @@ -38,7 +39,7 @@ export const CopyPasteMenu = ({ // We want to only copy properties that are currently in front of the user. // That includes search or any future filters. const currentStyleMap: StyleMap = new Map(); - for (const [property, value] of advancedStyles) { + for (const [property, value] of advancedStylesShorthands) { const isEmpty = toValue(value) === ""; if (properties.includes(property) && isEmpty === false) { currentStyleMap.set(hyphenateProperty(property), value); @@ -51,10 +52,14 @@ export const CopyPasteMenu = ({ const handleCopy = () => { const property = lastClickedProperty.current; + console.log(property); if (property === undefined) { return; } - const value = advancedStyles.get(property); + const value = + advancedStylesShorthands.get(property) ?? + advancedStylesLonghands.get(property); + if (value === undefined) { return; } @@ -71,9 +76,11 @@ export const CopyPasteMenu = ({ if (!(event.target instanceof HTMLElement)) { return; } - const property = event.target.closest( - `[${propertyContainerAttribute}]` - )?.dataset.property; + const property = + event.target + .closest(`[${copyAttribute}]`) + ?.getAttribute(copyAttribute) ?? undefined; + lastClickedProperty.current = property; }} > diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 29d93e628280..8e94a9ad3abd 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -24,7 +24,7 @@ const initialProperties = new Set([ "userSelect", ]); -export const $advancedStyles = computed( +export const $advancedStylesLonghands = computed( [ // prevent showing properties inherited from root // to not bloat advanced panel @@ -83,21 +83,28 @@ export const $advancedStyles = computed( advancedStyles.set(initialProperty, { type: "unset", value: "" }); } } - //console.log(advancedStyles); - const styles1: StyleMap = new Map(); - for (const [property, value] of advancedStyles) { - styles1.set(hyphenateProperty(property), value); + return advancedStyles; + } +); + +export const $advancedStylesShorthands = computed( + [$advancedStylesLonghands], + (advancedStylesLonghands) => { + const longhandsMap: StyleMap = new Map(); + // @todo this hyphen/camel case convesion needs to be solved by switching entirely to dash separated syntax. + for (const [property, value] of advancedStylesLonghands) { + longhandsMap.set(hyphenateProperty(property), value); } - const merged = mergeStyles(styles1); + const shorthands = mergeStyles(longhandsMap); - const styles2: StyleMap = new Map(); + const shorthandsMap: StyleMap = new Map(); - for (const [property, value] of merged) { - styles2.set(camelCase(property), value); + for (const [property, value] of shorthands) { + shorthandsMap.set(camelCase(property), value); } - console.log(styles2); - return styles2; + + return shorthandsMap; } ); diff --git a/apps/builder/app/builder/shared/collapsible-section.tsx b/apps/builder/app/builder/shared/collapsible-section.tsx index 7cbd4c263dcf..1d7f4998294f 100644 --- a/apps/builder/app/builder/shared/collapsible-section.tsx +++ b/apps/builder/app/builder/shared/collapsible-section.tsx @@ -132,31 +132,29 @@ export const CollapsibleSectionRoot = ({ }: CollapsibleSectionBaseProps) => { return ( - <> - - {trigger ?? ( - - {label} - - )} - - - - - {children} - - - - + + {trigger ?? ( + + {label} + + )} + + + + + {children} + + + ); }; From 2e07db5e85a758d9fdacf9757aacef2658d66923 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 22 Feb 2025 10:29:46 +0000 Subject: [PATCH 03/15] expand background position --- .../sections/advanced/advanced.tsx | 42 +++++++++---------- packages/css-engine/src/core/merger.ts | 1 + 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 13926c53d0b0..d8ffa0dba5da 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -383,7 +383,9 @@ const AdvancedDeclarationShorthand = memo( }) => { const { property, value, onReset } = props; const [isOpen, setIsOpen] = useState(false); - const longhands = expandShorthands([[property, toValue(value)]]); + const longhands = expandShorthands([ + [hyphenateProperty(property), toValue(value)], + ]); return ( @@ -559,26 +561,24 @@ export const Section = () => { ? AdvancedDeclarationShorthand : AdvancedDeclarationLonghand; return ( - - { - if (event.type === "enter") { - handleShowAddStyleInput(); - } - }} - onReset={() => { - updateRecentProperties( - recentProperties.filter( - (recentProperty) => recentProperty !== property - ) - ); - }} - /> - + { + if (event.type === "enter") { + handleShowAddStyleInput(); + } + }} + onReset={() => { + updateRecentProperties( + recentProperties.filter( + (recentProperty) => recentProperty !== property + ) + ); + }} + /> ); })} {(showRecentProperties || isAdding) && ( diff --git a/packages/css-engine/src/core/merger.ts b/packages/css-engine/src/core/merger.ts index 9e86c58f193c..95ffb247b56b 100644 --- a/packages/css-engine/src/core/merger.ts +++ b/packages/css-engine/src/core/merger.ts @@ -155,6 +155,7 @@ const supportedShorthandProperties = new Set([ "border-left", "white-space", "text-wrap", + "background-position", ]); export const isShorthand = (property: string) => { From b73e1e30ef0f3e73ac2f36a4f6e111147a468c4b Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 22 Feb 2025 12:01:07 +0000 Subject: [PATCH 04/15] use dash case as much as possible --- .../sections/advanced/advanced.tsx | 64 +++++++++++-------- .../style-panel/sections/advanced/stores.ts | 24 +++---- packages/css-engine/src/core/merger.ts | 7 +- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index d8ffa0dba5da..34ba253f6a60 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -29,6 +29,7 @@ import { Tooltip, } from "@webstudio-is/design-system"; import { + camelCaseProperty, expandShorthands, parseCssValue, propertyDescriptions, @@ -38,7 +39,7 @@ import { isShorthand, StyleValue, toValue, - type StyleProperty, + type CssProperty, } from "@webstudio-is/css-engine"; import { CollapsibleSectionRoot, @@ -66,20 +67,19 @@ import { $settings } from "~/builder/shared/client-settings"; import { AddStyleInput } from "./add-style-input"; import { parseStyleInput } from "./parse-style-input"; import { $selectedInstanceKey } from "~/shared/awareness"; -import { camelCase } from "change-case"; // Only here to keep the same section module interface export const properties = []; const AdvancedStyleSection = (props: { label: string; - properties: StyleProperty[]; + properties: Array; onAdd: () => void; children: ReactNode; }) => { const { label, children, properties, onAdd } = props; const [isOpen, setIsOpen] = useOpenState(label); - const styles = useComputedStyles(properties); + const styles = useComputedStyles(properties.map(camelCaseProperty)); return ( void; }) => { - const styleDecl = useComputedStyleDecl(property); + const camelCasedProperty = camelCaseProperty(property); + const styleDecl = useComputedStyleDecl(camelCasedProperty); const label = hyphenateProperty(property); const description = propertyDescriptions[property]; const [isOpen, setIsOpen] = useState(false); @@ -146,7 +147,7 @@ const AdvancedPropertyLabel = ({ onClick: (event) => { if (event.altKey) { event.preventDefault(); - deleteProperty(property); + deleteProperty(camelCasedProperty); onReset?.(); return; } @@ -159,7 +160,7 @@ const AdvancedPropertyLabel = ({ description={description} styles={[styleDecl]} onReset={() => { - deleteProperty(property); + deleteProperty(camelCasedProperty); setIsOpen(false); onReset?.(); }} @@ -189,14 +190,16 @@ const AdvancedPropertyValue = ({ inputRef: inputRefProp, }: { autoFocus?: boolean; - property: StyleProperty; + property: CssProperty; onChangeComplete: ComponentProps< typeof CssValueInputContainer >["onChangeComplete"]; onReset: ComponentProps["onReset"]; inputRef?: RefObject; }) => { - const styleDecl = useComputedStyleDecl(property); + // @todo conversion should be removed once data is in dash case + const camelCasedProperty = camelCaseProperty(property); + const styleDecl = useComputedStyleDecl(camelCasedProperty); const inputRef = useRef(null); useEffect(() => { if (autoFocus) { @@ -218,21 +221,21 @@ const AdvancedPropertyValue = ({ onChange={(styleValue) => { const options = { isEphemeral: true, listed: true }; if (styleValue) { - setProperty(property)(styleValue, options); + setProperty(camelCasedProperty)(styleValue, options); } else { - deleteProperty(property, options); + deleteProperty(camelCasedProperty, options); } }} onChangeComplete={(styleValue) => { - setProperty(property)(styleValue); + setProperty(camelCasedProperty)(styleValue); }} /> ) } - property={property} + property={camelCasedProperty} styleSource={styleDecl.source.name} getOptions={() => [ - ...styleConfigByName(property).items.map((item) => ({ + ...styleConfigByName(camelCasedProperty).items.map((item) => ({ type: "keyword" as const, value: item.name, })), @@ -244,12 +247,15 @@ const AdvancedPropertyValue = ({ styleValue.type === "keyword" && styleValue.value.startsWith("--") ) { - setProperty(property)( + setProperty(camelCasedProperty)( { type: "var", value: styleValue.value.slice(2) }, { ...options, listed: true } ); } else { - setProperty(property)(styleValue, { ...options, listed: true }); + setProperty(camelCasedProperty)(styleValue, { + ...options, + listed: true, + }); } }} deleteProperty={deleteProperty} @@ -329,7 +335,7 @@ const AdvancedDeclarationLonghand = memo( valueInputRef, indentation = initialIndentation, }: { - property: StyleProperty; + property: CssProperty; value: StyleValue | undefined; autoFocus?: boolean; indentation?: string; @@ -372,7 +378,7 @@ const AdvancedDeclarationLonghand = memo( const AdvancedDeclarationShorthand = memo( (props: { - property: StyleProperty; + property: CssProperty; value: StyleValue | undefined; autoFocus?: boolean; onReset?: () => void; @@ -386,7 +392,7 @@ const AdvancedDeclarationShorthand = memo( const longhands = expandShorthands([ [hyphenateProperty(property), toValue(value)], ]); - + const camelCasedProperty = camelCaseProperty(property); return ( @@ -426,8 +432,8 @@ const AdvancedDeclarationShorthand = memo( {...props} key={property} indentation="30px" - property={camelCase(property) as StyleProperty} - value={parseCssValue(property as StyleProperty, value)} + property={property} + value={parseCssValue(camelCasedProperty, value)} /> ); })} @@ -444,19 +450,19 @@ export const Section = () => { // Memorizing recent properties by instance, so that when user switches between instances and comes back // they are still in-place const [recentPropertiesMap, setRecentPropertiesMap] = useState< - Map> + Map> >(new Map()); const addPropertyInputRef = useRef(null); const recentValueInputRef = useRef(null); const searchInputRef = useRef(null); const [searchProperties, setSearchProperties] = - useState>(); + useState>(); const containerRef = useRef(null); const [minHeight, setMinHeight] = useState(0); const advancedProperties = Array.from( advancedStyles.keys() - ) as Array; + ) as Array; const currentProperties = searchProperties ?? advancedProperties; @@ -471,7 +477,7 @@ export const Section = () => { setMinHeight(containerRef.current?.getBoundingClientRect().height ?? 0); }; - const updateRecentProperties = (properties: Array) => { + const updateRecentProperties = (properties: Array) => { if (selectedInstanceKey === undefined) { return; } @@ -485,7 +491,9 @@ export const Section = () => { const handleInsertStyles = (cssText: string) => { const styles = insertStyles(cssText); - const insertedProperties = styles.map(({ property }) => property); + const insertedProperties = styles.map( + ({ property }) => hyphenateProperty(property) as CssProperty + ); updateRecentProperties(insertedProperties); return styles; }; @@ -520,7 +528,7 @@ export const Section = () => { keys: ["property", "value"], }).map(({ property }) => property); - setSearchProperties(matched as StyleProperty[]); + setSearchProperties(matched as CssProperty[]); }; const handleAbortAddStyles = () => { diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 8e94a9ad3abd..5e9249164816 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -2,8 +2,8 @@ import { computed } from "nanostores"; import { hyphenateProperty, mergeStyles, + type CssProperty, type StyleMap, - type StyleProperty, } from "@webstudio-is/css-engine"; import { $matchingBreakpoints, getDefinedStyles } from "../../shared/model"; import { sections } from "../sections"; @@ -14,14 +14,13 @@ import { } from "~/shared/nano-states"; import { $selectedInstancePath } from "~/shared/awareness"; import { $settings } from "~/builder/shared/client-settings"; -import { camelCase } from "change-case"; -const initialProperties = new Set([ +const initialProperties = new Set([ "cursor", - "mixBlendMode", + "mix-blend-mode", "opacity", - "pointerEvents", - "userSelect", + "pointer-events", + "user-select", ]); export const $advancedStylesLonghands = computed( @@ -58,22 +57,23 @@ export const $advancedStylesLonghands = computed( }); // All properties used by the panels except the advanced panel - const visualProperties = new Set([]); + const visualProperties = new Set([]); for (const { properties } of sections.values()) { for (const property of properties) { - visualProperties.add(property); + visualProperties.add(hyphenateProperty(property) as CssProperty); } } for (const style of definedStyles) { const { property, value, listed } = style; + const hyphenatedProperty = hyphenateProperty(property) as CssProperty; // When property is listed, it was added from advanced panel. // If we are in advanced mode, we show them all. if ( - visualProperties.has(property) === false || + visualProperties.has(hyphenatedProperty) === false || listed || settings.stylePanelMode === "advanced" ) { - advancedStyles.set(property, value); + advancedStyles.set(hyphenatedProperty, value); } } // In advanced mode we assume user knows the properties they need, so we don't need to show these. @@ -94,7 +94,7 @@ export const $advancedStylesShorthands = computed( const longhandsMap: StyleMap = new Map(); // @todo this hyphen/camel case convesion needs to be solved by switching entirely to dash separated syntax. for (const [property, value] of advancedStylesLonghands) { - longhandsMap.set(hyphenateProperty(property), value); + longhandsMap.set(property, value); } const shorthands = mergeStyles(longhandsMap); @@ -102,7 +102,7 @@ export const $advancedStylesShorthands = computed( const shorthandsMap: StyleMap = new Map(); for (const [property, value] of shorthands) { - shorthandsMap.set(camelCase(property), value); + shorthandsMap.set(property, value); } return shorthandsMap; diff --git a/packages/css-engine/src/core/merger.ts b/packages/css-engine/src/core/merger.ts index 95ffb247b56b..8f22baba05f6 100644 --- a/packages/css-engine/src/core/merger.ts +++ b/packages/css-engine/src/core/merger.ts @@ -1,9 +1,4 @@ -import { - StyleValue, - TupleValue, - TupleValueItem, - type StyleProperty, -} from "../schema"; +import { StyleValue, TupleValue, TupleValueItem } from "../schema"; import { cssWideKeywords } from "../css"; import type { StyleMap } from "./rules"; import { toValue } from "./to-value"; From 5eeb250ebbd4fca414888b05a15ab29ba28ac6ee Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 22 Feb 2025 12:04:32 +0000 Subject: [PATCH 05/15] delete unnecessary conversion --- .../features/style-panel/sections/advanced/stores.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 5e9249164816..96548ecd17d3 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -15,6 +15,7 @@ import { import { $selectedInstancePath } from "~/shared/awareness"; import { $settings } from "~/builder/shared/client-settings"; +// @todo will be fully deleted https://github.com/webstudio-is/webstudio/issues/4871 const initialProperties = new Set([ "cursor", "mix-blend-mode", @@ -91,15 +92,8 @@ export const $advancedStylesLonghands = computed( export const $advancedStylesShorthands = computed( [$advancedStylesLonghands], (advancedStylesLonghands) => { - const longhandsMap: StyleMap = new Map(); - // @todo this hyphen/camel case convesion needs to be solved by switching entirely to dash separated syntax. - for (const [property, value] of advancedStylesLonghands) { - longhandsMap.set(property, value); - } - - const shorthands = mergeStyles(longhandsMap); - const shorthandsMap: StyleMap = new Map(); + const shorthands = mergeStyles(advancedStylesLonghands); for (const [property, value] of shorthands) { shorthandsMap.set(property, value); From adec4e3ba9eef8de06c52217855baa7eb81ade0d Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sun, 23 Feb 2025 11:09:27 +0000 Subject: [PATCH 06/15] cleanup mdn-data script --- packages/css-data/bin/mdn-data.ts | 157 +++++++++--------- .../css-engine/src/__generated__/types.ts | 1 + 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/packages/css-data/bin/mdn-data.ts b/packages/css-data/bin/mdn-data.ts index ca7445e6581a..6d19cf84da7b 100755 --- a/packages/css-data/bin/mdn-data.ts +++ b/packages/css-data/bin/mdn-data.ts @@ -223,6 +223,18 @@ const walkSyntax = ( walk(parsed); }; +const autogeneratedHint = "// This file was generated by pnpm mdn-data\n"; + +const writeToFile = (fileName: string, constant: string, data: unknown) => { + const content = + autogeneratedHint + + `export const ${constant} = ` + + JSON.stringify(data, null, 2) + + " as const;"; + + writeFileSync(join(targetDir, fileName), content, "utf8"); +}; + type FilteredProperties = { [property: string]: Value }; const experimentalProperties = [ @@ -255,6 +267,7 @@ const unsupportedProperties = [ ]; const animatableProperties: string[] = []; + const filteredProperties: FilteredProperties = (() => { let property: Property; const result = {} as FilteredProperties; @@ -285,44 +298,48 @@ const filteredProperties: FilteredProperties = (() => { return result; })(); -const propertiesData = { - ...customData.propertiesData, -}; +const getPropertiesData = () => { + const propertiesData = { + ...customData.propertiesData, + }; -let property: string; -for (property in filteredProperties) { - const config = filteredProperties[property]; - const unitGroups = new Set(); - walkSyntax(config.syntax, (node) => { - if (node.type === "Type") { - if (node.name === "integer" || node.name === "number") { - unitGroups.add("number"); - return; - } + let property: string; + for (property in filteredProperties) { + const config = filteredProperties[property]; + const unitGroups = new Set(); + walkSyntax(config.syntax, (node) => { + if (node.type === "Type") { + if (node.name === "integer" || node.name === "number") { + unitGroups.add("number"); + return; + } - // type names match unit groups - if (node.name in units) { - unitGroups.add(node.name); - return; + // type names match unit groups + if (node.name in units) { + unitGroups.add(node.name); + return; + } } + }); + + if (Array.isArray(config.initial)) { + throw new Error( + `Property ${property} contains non string initial value ${config.initial.join( + ", " + )}` + ); } - }); - if (Array.isArray(config.initial)) { - throw new Error( - `Property ${property} contains non string initial value ${config.initial.join( - ", " - )}` - ); + propertiesData[camelCaseProperty(property as CssProperty)] = { + unitGroups: Array.from(unitGroups), + inherited: config.inherited, + initial: parseInitialValue(property, config.initial, unitGroups), + ...("mdn_url" in config && { mdnUrl: config.mdn_url }), + }; } - propertiesData[camelCaseProperty(property as CssProperty)] = { - unitGroups: Array.from(unitGroups), - inherited: config.inherited, - initial: parseInitialValue(property, config.initial, unitGroups), - ...("mdn_url" in config && { mdnUrl: config.mdn_url }), - }; -} + return propertiesData; +}; const pseudoElements = Object.keys(selectors) .filter((selector) => { @@ -330,31 +347,14 @@ const pseudoElements = Object.keys(selectors) }) .map((selector) => selector.slice(2)); -const targetDir = join(process.cwd(), process.argv.slice(2).pop() as string); - -mkdirSync(targetDir, { recursive: true }); - -const writeToFile = (fileName: string, constant: string, data: unknown) => { - const autogeneratedHint = "// This file was generated by pnpm mdn-data\n"; - const content = - autogeneratedHint + - `export const ${constant} = ` + - JSON.stringify(data, null, 2) + - " as const;"; - - writeFileSync(join(targetDir, fileName), content, "utf8"); -}; - -// Non-standard properties are just missing in mdn data -const nonStandardValues = { - "background-clip": ["text"], -}; - -// https://www.w3.org/TR/css-values/#common-keywords -const commonKeywords = ["initial", "inherit", "unset"]; - -const keywordValues = (() => { +const getKeywordValues = () => { const result = { ...customData.keywordValues }; + // Non-standard properties are just missing in mdn data + const nonStandardValues = { + "background-clip": ["text"], + }; + // https://www.w3.org/TR/css-values/#common-keywords + const commonKeywords = ["initial", "inherit", "unset"]; for (const property in filteredProperties) { const key = camelCaseProperty(property as CssProperty); @@ -391,38 +391,43 @@ const keywordValues = (() => { } return result; -})(); +}; + +const getTypes = (propertiesData: typeof customData.propertiesData) => { + let types = ""; + + const camelCasedProperties = Object.keys(propertiesData).map((property) => + JSON.stringify(property) + ); + types += `export type CamelCasedProperty = ${camelCasedProperties.join(" | ")};\n\n`; + const hyphenatedProperties = Object.keys(propertiesData).map((property) => + JSON.stringify(hyphenateProperty(property)) + ); + types += `export type HyphenatedProperty = ${hyphenatedProperties.join(" | ")};\n\n`; + + const unitLiterals = Object.values(units) + .flat() + .map((unit) => JSON.stringify(unit)); + types += `export type Unit = ${unitLiterals.join(" | ")};\n`; + return types; +}; + +const targetDir = join(process.cwd(), process.argv.slice(2).pop() as string); +mkdirSync(targetDir, { recursive: true }); writeToFile("units.ts", "units", units); -writeToFile("properties.ts", "properties", propertiesData); -writeToFile("keyword-values.ts", "keywordValues", keywordValues); +writeToFile("properties.ts", "properties", getPropertiesData()); +writeToFile("keyword-values.ts", "keywordValues", getKeywordValues()); writeToFile( "animatable-properties.ts", "animatableProperties", animatableProperties ); - writeToFile("pseudo-elements.ts", "pseudoElements", pseudoElements); -let types = ""; - -const camelCasedProperties = Object.keys(propertiesData).map((property) => - JSON.stringify(property) -); -types += `export type CamelCasedProperty = ${camelCasedProperties.join(" | ")};\n\n`; -const hyphenatedProperties = Object.keys(propertiesData).map((property) => - JSON.stringify(hyphenateProperty(property)) -); -types += `export type HyphenatedProperty = ${hyphenatedProperties.join(" | ")};\n\n`; - -const unitLiterals = Object.values(units) - .flat() - .map((unit) => JSON.stringify(unit)); -types += `export type Unit = ${unitLiterals.join(" | ")};\n`; - const typesFile = join( process.cwd(), "../css-engine/src/__generated__/types.ts" ); mkdirSync(dirname(typesFile), { recursive: true }); -writeFileSync(typesFile, types); +writeFileSync(typesFile, autogeneratedHint + getTypes(getPropertiesData())); diff --git a/packages/css-engine/src/__generated__/types.ts b/packages/css-engine/src/__generated__/types.ts index ff109f02b318..3e2a6d185858 100644 --- a/packages/css-engine/src/__generated__/types.ts +++ b/packages/css-engine/src/__generated__/types.ts @@ -1,3 +1,4 @@ +// This file was generated by pnpm mdn-data export type CamelCasedProperty = | "WebkitFontSmoothing" | "MozOsxFontSmoothing" From 481878dcd3141614b7e241d96ebb761dc3b292fb Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sun, 23 Feb 2025 12:06:13 +0000 Subject: [PATCH 07/15] generate shorthand properties --- packages/css-data/bin/mdn-data.ts | 96 +++++++++++++------ .../src/__generated__/shorthand-properties.ts | 60 ++++++++++++ 2 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 packages/css-data/src/__generated__/shorthand-properties.ts diff --git a/packages/css-data/bin/mdn-data.ts b/packages/css-data/bin/mdn-data.ts index 6d19cf84da7b..b28f2477e46f 100755 --- a/packages/css-data/bin/mdn-data.ts +++ b/packages/css-data/bin/mdn-data.ts @@ -235,9 +235,7 @@ const writeToFile = (fileName: string, constant: string, data: unknown) => { writeFileSync(join(targetDir, fileName), content, "utf8"); }; -type FilteredProperties = { [property: string]: Value }; - -const experimentalProperties = [ +const supportedExperimentalProperties = [ "appearance", "aspect-ratio", "text-size-adjust", @@ -254,6 +252,7 @@ const experimentalProperties = [ "offset-anchor", ]; +// Properties we don't support in this form. const unsupportedProperties = [ "--*", // shorthand properties @@ -266,42 +265,69 @@ const unsupportedProperties = [ "background-position", ]; -const animatableProperties: string[] = []; +type FilteredProperties = { [property: string]: Value }; -const filteredProperties: FilteredProperties = (() => { +const filterData = () => { let property: Property; - const result = {} as FilteredProperties; + const allLonghands = {} as FilteredProperties; + const allShorthands = {} as FilteredProperties; + const animatableLonghands = {} as FilteredProperties; + const animatableShorthands = {} as FilteredProperties; for (property in properties) { + if (unsupportedProperties.includes(property)) { + continue; + } + const config = properties[property]; - const isSupportedProperty = - // make sure the property standard and described in mdn - (config.status === "standard" && "mdn_url" in config) || - experimentalProperties.includes(property); - const isShorthandProperty = Array.isArray(config.initial); + const isStandardProperty = + config.status === "standard" && "mdn_url" in config; + + const isSupportedExperimentalProperty = + supportedExperimentalProperties.includes(property); + + if ( + isStandardProperty === false && + isSupportedExperimentalProperty === false + ) { + continue; + } + const isAnimatableProperty = property.startsWith("-") === false && config.animationType !== "discrete" && config.animationType !== "notAnimatable"; - if (unsupportedProperties.includes(property) || isShorthandProperty) { - continue; - } - if (isSupportedProperty) { + const isShorthandProperty = Array.isArray(config.initial); + + if (isShorthandProperty) { + allShorthands[property] = config; if (isAnimatableProperty) { - animatableProperties.push(property); + animatableShorthands[property] = config; } - result[property as Property] = config; + continue; + } + + allLonghands[property] = config; + if (isAnimatableProperty) { + animatableLonghands[property] = config; } } - return result; -})(); -const getPropertiesData = () => { - const propertiesData = { - ...customData.propertiesData, + return { + allLonghands, + allShorthands, + animatableLonghands, + animatableShorthands, }; +}; + +const getPropertiesData = ( + customPropertiesData: typeof customData.propertiesData, + filteredProperties: FilteredProperties +) => { + const propertiesData = { ...customPropertiesData }; let property: string; for (property in filteredProperties) { @@ -347,7 +373,7 @@ const pseudoElements = Object.keys(selectors) }) .map((selector) => selector.slice(2)); -const getKeywordValues = () => { +const getKeywordValues = (filteredProperties: FilteredProperties) => { const result = { ...customData.keywordValues }; // Non-standard properties are just missing in mdn data const nonStandardValues = { @@ -413,15 +439,31 @@ const getTypes = (propertiesData: typeof customData.propertiesData) => { return types; }; +const filteredData = filterData(); + +const longhandPropertiesData = getPropertiesData( + customData.propertiesData, + filteredData.allLonghands +); + const targetDir = join(process.cwd(), process.argv.slice(2).pop() as string); mkdirSync(targetDir, { recursive: true }); writeToFile("units.ts", "units", units); -writeToFile("properties.ts", "properties", getPropertiesData()); -writeToFile("keyword-values.ts", "keywordValues", getKeywordValues()); +writeToFile("properties.ts", "properties", longhandPropertiesData); +writeToFile( + "shorthand-properties.ts", + "shorthandProperties", + Object.keys(filteredData.allShorthands) +); +writeToFile( + "keyword-values.ts", + "keywordValues", + getKeywordValues(filteredData.allLonghands) +); writeToFile( "animatable-properties.ts", "animatableProperties", - animatableProperties + Object.keys(filteredData.animatableLonghands) ); writeToFile("pseudo-elements.ts", "pseudoElements", pseudoElements); @@ -430,4 +472,4 @@ const typesFile = join( "../css-engine/src/__generated__/types.ts" ); mkdirSync(dirname(typesFile), { recursive: true }); -writeFileSync(typesFile, autogeneratedHint + getTypes(getPropertiesData())); +writeFileSync(typesFile, autogeneratedHint + getTypes(longhandPropertiesData)); diff --git a/packages/css-data/src/__generated__/shorthand-properties.ts b/packages/css-data/src/__generated__/shorthand-properties.ts new file mode 100644 index 000000000000..8295f27ffe4a --- /dev/null +++ b/packages/css-data/src/__generated__/shorthand-properties.ts @@ -0,0 +1,60 @@ +// This file was generated by pnpm mdn-data +export const shorthandProperties = [ + "animation", + "background", + "border", + "border-block", + "border-block-end", + "border-block-start", + "border-bottom", + "border-color", + "border-image", + "border-inline", + "border-inline-end", + "border-inline-start", + "border-left", + "border-radius", + "border-right", + "border-style", + "border-top", + "border-width", + "column-rule", + "columns", + "contain-intrinsic-size", + "container", + "flex", + "flex-flow", + "font", + "gap", + "grid", + "grid-area", + "grid-column", + "grid-row", + "grid-template", + "inset", + "inset-block", + "inset-inline", + "list-style", + "margin", + "margin-block", + "margin-inline", + "mask", + "mask-border", + "offset", + "outline", + "padding", + "padding-block", + "padding-inline", + "place-content", + "place-items", + "place-self", + "scroll-margin", + "scroll-margin-block", + "scroll-margin-inline", + "scroll-padding", + "scroll-padding-block", + "scroll-padding-inline", + "text-decoration", + "text-emphasis", + "transition", +] as const; From c968388775716112a746a4e81c227aef536ac4c6 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 25 Feb 2025 16:29:29 +0000 Subject: [PATCH 08/15] wip --- .../sections/advanced/add-style-input.tsx | 32 ++-- .../sections/advanced/advanced.tsx | 139 +++++++++++------- .../advanced/parse-style-input.test.ts | 122 ++++++--------- .../sections/advanced/parse-style-input.ts | 95 ++++++------ .../style-panel/sections/advanced/stores.ts | 5 +- .../css-value-input/css-value-input.tsx | 2 +- packages/css-data/src/index.ts | 1 + packages/css-engine/src/core/index.ts | 2 +- packages/css-engine/src/core/merger.ts | 52 +++++-- packages/css-engine/src/schema.ts | 4 + 10 files changed, 242 insertions(+), 212 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx index e97fc858bfba..6cc8b6410726 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx @@ -17,6 +17,7 @@ import { } from "@webstudio-is/design-system"; import { properties as propertiesData, + shorthandProperties, keywordValues, propertyDescriptions, parseCssValue, @@ -25,6 +26,7 @@ import { cssWideKeywords, generateStyleMap, hyphenateProperty, + mergeStyles, toValue, type StyleProperty, } from "@webstudio-is/css-engine"; @@ -55,6 +57,13 @@ const getAutocompleteItems = () => { }); } + for (const property of shorthandProperties) { + autoCompleteItems.push({ + property, + label: property, + }); + } + const ignoreValues = new Set([...cssWideKeywords, ...keywordValues.color]); for (const property in keywordValues) { @@ -91,21 +100,23 @@ const matchOrSuggestToCreate = ( matched.length = Math.min(matched.length, 100); if (matched.length === 0) { - const parsedStyles = parseStyleInput(search); + const parsedStyleMap = parseStyleInput(search); + const styleMap = mergeStyles(parsedStyleMap); + // When parsedStyles is more than one, user entered a shorthand. // We will suggest to insert their shorthand first. - if (parsedStyles.length > 1) { + if (styleMap.size > 1) { matched.push({ property: search, label: `Create "${search}"`, }); } // Now we will suggest to insert each longhand separately. - for (const style of parsedStyles) { + for (const [property, value] of styleMap) { matched.push({ - property: style.property, - value: toValue(style.value), - label: `Create "${generateStyleMap(new Map([[style.property, style.value]]))}"`, + property: property, + value: toValue(value), + label: `Create "${generateStyleMap(new Map([[property, value]]))}"`, }); } } @@ -114,13 +125,12 @@ const matchOrSuggestToCreate = ( }; /** - * * Advanced search control supports following interactions * - * find property - * create custom property - * submit css declarations - * paste css declarations + * - find property + * - create custom property + * - submit css declarations + * - paste css declarations * */ export const AddStyleInput = forwardRef< diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 34ba253f6a60..08eb43f55db6 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -37,7 +37,9 @@ import { import { hyphenateProperty, isShorthand, + mergeStyles, StyleValue, + supportedShorthands, toValue, type CssProperty, } from "@webstudio-is/css-engine"; @@ -62,7 +64,7 @@ import { PropertyInfo } from "../../property-label"; import { ColorPopover } from "../../shared/color-picker"; import { useClientSupports } from "~/shared/client-supports"; import { CopyPasteMenu, copyAttribute } from "./copy-paste-menu"; -import { $advancedStylesShorthands } from "./stores"; +import { $advancedStylesLonghands, $advancedStylesShorthands } from "./stores"; import { $settings } from "~/builder/shared/client-settings"; import { AddStyleInput } from "./add-style-input"; import { parseStyleInput } from "./parse-style-input"; @@ -109,16 +111,16 @@ const AdvancedStyleSection = (props: { }; const insertStyles = (css: string) => { - const parsedStyles = parseStyleInput(css); - if (parsedStyles.length === 0) { - return []; + const styleMap = parseStyleInput(css); + if (styleMap.size === 0) { + return new Map(); } const batch = createBatchUpdate(); - for (const { property, value } of parsedStyles) { - batch.setProperty(property)(value); + for (const [property, value] of styleMap) { + batch.setProperty(camelCaseProperty(property as CssProperty))(value); } batch.publish({ listed: true }); - return parsedStyles; + return styleMap; }; // Used to indent the values when they are on the next line. This way its easier to see @@ -208,6 +210,7 @@ const AdvancedPropertyValue = ({ } }, [autoFocus]); const isColor = colord(toValue(styleDecl.usedValue)).isValid(); + return ( @@ -443,11 +447,31 @@ const AdvancedDeclarationShorthand = memo( } ); +const toShorthands = (properties: Array) => { + console.log("properties", properties); + const shorthands = new Set(); + for (const property of properties) { + let isAdded = false; + for (const [shorthand, longhands] of supportedShorthands) { + if (longhands.has(property)) { + shorthands.add(shorthand); + isAdded = true; + break; + } + } + if (isAdded === false) { + shorthands.add(property); + } + } + console.log("shorthands", shorthands); + return Array.from(shorthands); +}; + export const Section = () => { const [isAdding, setIsAdding] = useState(false); - const advancedStyles = useStore($advancedStylesShorthands); + const advancedStyles = useStore($advancedStylesLonghands); const selectedInstanceKey = useStore($selectedInstanceKey); - // Memorizing recent properties by instance, so that when user switches between instances and comes back + // Memorizing recent properties by instance id, so that when user switches between instances and comes back // they are still in-place const [recentPropertiesMap, setRecentPropertiesMap] = useState< Map> @@ -490,12 +514,12 @@ export const Section = () => { }; const handleInsertStyles = (cssText: string) => { - const styles = insertStyles(cssText); - const insertedProperties = styles.map( - ({ property }) => hyphenateProperty(property) as CssProperty - ); + const styleMap = insertStyles(cssText); + const insertedProperties = Array.from( + styleMap.keys() + ) as Array; updateRecentProperties(insertedProperties); - return styles; + return styleMap; }; const handleShowAddStyleInput = () => { @@ -540,6 +564,7 @@ export const Section = () => { }); }; + console.log({ recentProperties, currentProperties }); return ( { css={{ paddingInline: theme.panel.paddingInline, gap: 2 }} > {showRecentProperties && - recentProperties.map((property, index, properties) => { - const isLast = index === properties.length - 1; - const AdvancedDeclaration = isShorthand(property) - ? AdvancedDeclarationShorthand - : AdvancedDeclarationLonghand; - return ( - { - if (event.type === "enter") { - handleShowAddStyleInput(); - } - }} - onReset={() => { - updateRecentProperties( - recentProperties.filter( - (recentProperty) => recentProperty !== property - ) - ); - }} - /> - ); - })} + toShorthands(recentProperties).map( + (property, index, properties) => { + const isLast = index === properties.length - 1; + const AdvancedDeclaration = isShorthand(property) + ? AdvancedDeclarationShorthand + : AdvancedDeclarationLonghand; + return ( + { + if (event.type === "enter") { + handleShowAddStyleInput(); + } + }} + onReset={() => { + updateRecentProperties( + recentProperties.filter( + (recentProperty) => recentProperty !== property + ) + ); + }} + /> + ); + } + )} {(showRecentProperties || isAdding) && ( { { const styles = handleInsertStyles(cssText); - if (styles.length > 0) { + if (styles.size > 0) { setIsAdding(false); } }} @@ -626,23 +653,23 @@ export const Section = () => { style={{ minHeight }} ref={containerRef} > - {currentProperties - .filter( + {toShorthands( + currentProperties.filter( (property) => recentProperties.includes(property) === false ) - .map((property) => { - const AdvancedDeclaration = isShorthand(property) - ? AdvancedDeclarationShorthand - : AdvancedDeclarationLonghand; - return ( - - - - ); - })} + ).map((property) => { + const AdvancedDeclaration = isShorthand(property) + ? AdvancedDeclarationShorthand + : AdvancedDeclarationLonghand; + return ( + + + + ); + })} diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.test.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.test.ts index 075f9e8a3a06..8816aef28395 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.test.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.test.ts @@ -4,115 +4,87 @@ import { parseStyleInput } from "./parse-style-input"; describe("parseStyleInput", () => { test("parses custom property", () => { const result = parseStyleInput("--custom-color"); - expect(result).toEqual([ - { - selector: "selector", - property: "--custom-color", - value: { type: "unset", value: "" }, - }, - ]); + expect(result).toEqual( + new Map([["--custom-color", { type: "keyword", value: "unset" }]]) + ); }); - test("parses regular property", () => { + test("parses longhand property", () => { const result = parseStyleInput("color"); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "unset", value: "" }, - }, - ]); + expect(result).toEqual( + new Map([["color", { type: "keyword", value: "unset" }]]) + ); + }); + + test("parses shorthand property", () => { + const result = parseStyleInput("margin"); + expect(result).toEqual( + new Map([ + ["margin-top", { type: "keyword", value: "unset" }], + ["margin-right", { type: "keyword", value: "unset" }], + ["margin-bottom", { type: "keyword", value: "unset" }], + ["margin-left", { type: "keyword", value: "unset" }], + ]) + ); }); test("trims whitespace", () => { const result = parseStyleInput(" color "); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "unset", value: "" }, - }, - ]); + expect(result).toEqual( + new Map([["color", { type: "keyword", value: "unset" }]]) + ); }); test("handles unparsable regular property", () => { const result = parseStyleInput("notapro perty"); - expect(result).toEqual([]); + expect(result).toEqual(new Map()); }); test("converts unknown property to custom property assuming user forgot to add --", () => { const result = parseStyleInput("notaproperty"); - expect(result).toEqual([ - { - selector: "selector", - property: "--notaproperty", - value: { type: "unset", value: "" }, - }, - ]); + expect(result).toEqual( + new Map([["--notaproperty", { type: "keyword", value: "unset" }]]) + ); }); test("parses single property-value pair", () => { const result = parseStyleInput("color: red"); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "keyword", value: "red" }, - }, - ]); + expect(result).toEqual( + new Map([["color", { type: "keyword", value: "red" }]]) + ); }); test("parses multiple property-value pairs", () => { const result = parseStyleInput("color: red; display: block"); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "keyword", value: "red" }, - }, - { - selector: "selector", - property: "display", - value: { type: "keyword", value: "block" }, - }, - ]); + expect(result).toEqual( + new Map([ + ["color", { type: "keyword", value: "red" }], + ["display", { type: "keyword", value: "block" }], + ]) + ); }); test("parses multiple property-value pairs, one is invalid", () => { const result = parseStyleInput("color: red; somethinginvalid: block"); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "keyword", value: "red" }, - }, - { - selector: "selector", - property: "--somethinginvalid", - value: { type: "unparsed", value: "block" }, - }, - ]); + expect(result).toEqual( + new Map([ + ["color", { type: "keyword", value: "red" }], + ["--somethinginvalid", { type: "unparsed", value: "block" }], + ]) + ); }); test("parses custom property with value", () => { const result = parseStyleInput("--custom-color: red"); - expect(result).toEqual([ - { - selector: "selector", - property: "--custom-color", - value: { type: "unparsed", value: "red" }, - }, - ]); + expect(result).toEqual( + new Map([["--custom-color", { type: "unparsed", value: "red" }]]) + ); }); test("handles malformed style block", () => { const result = parseStyleInput("color: red; invalid;"); - expect(result).toEqual([ - { - selector: "selector", - property: "color", - value: { type: "keyword", value: "red" }, - }, - ]); + expect(result).toEqual( + new Map([["color", { type: "keyword", value: "red" }]]) + ); }); }); diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts index f58cead297b9..ce7f0f679f53 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts @@ -1,14 +1,40 @@ import { - type ParsedStyleDecl, properties, parseCss, camelCaseProperty, + shorthandProperties, } from "@webstudio-is/css-data"; -import type { CssProperty, StyleProperty } from "@webstudio-is/css-engine"; +import { + mergeStyles, + type CssProperty, + type StyleMap, +} from "@webstudio-is/css-engine"; import { lexer } from "css-tree"; -type StyleDecl = Omit & { - property: StyleProperty; +// When user provides only a property name, we need to make it `property:;` to be able to parse it. +const ensureValue = (css: string) => { + css = css.trim(); + + // Is it a custom property "--foo"? + if (css.startsWith("--") && lexer.match("", css).matched) { + return `${css}:;`; + } + // Is it a known longhand property? + if (camelCaseProperty(css as CssProperty) in properties) { + return `${css}:;`; + } + // Is it a known shorthand property? + if ( + shorthandProperties.includes(css as (typeof shorthandProperties)[number]) + ) { + return `${css}:;`; + } + // Is it a custom property without dashes "--foo"? + if (lexer.match("", `--${css}`).matched) { + return `--${css}:;`; + } + + return css; }; /** @@ -20,64 +46,27 @@ type StyleDecl = Omit & { * - Property and value: color: red * - Multiple properties: color: red; background: blue */ -export const parseStyleInput = (css: string): Array => { - css = css.trim(); - // Is it a custom property "--foo"? - if (css.startsWith("--") && lexer.match("", css).matched) { - return [ - { - selector: "selector", - property: css as StyleProperty, - value: { type: "unset", value: "" }, - }, - ]; - } +export const parseStyleInput = (css: string): StyleMap => { + css = ensureValue(css); - // Is it a known regular property? - if (camelCaseProperty(css as CssProperty) in properties) { - return [ - { - selector: "selector", - property: camelCaseProperty(css as CssProperty), - value: { type: "unset", value: "" }, - }, - ]; - } + const styles = parseCss(`selector{${css}}`); - // Is it a custom property "--foo"? - if (lexer.match("", `--${css}`).matched) { - return [ - { - selector: "selector", - property: `--${css}`, - value: { type: "unset", value: "" }, - }, - ]; - } + const styleMap: StyleMap = new Map(); - const hyphenatedStyles = parseCss(`selector{${css}}`); - const newStyles: StyleDecl[] = []; - - for (const { property, ...styleDecl } of hyphenatedStyles) { + for (const { property, value } of styles) { // somethingunknown: red; -> --somethingunknown: red; if ( // Note: currently in tests it returns unparsed, but in the client it returns invalid, // because we use native APIs when available in parseCss. - styleDecl.value.type === "invalid" || - (styleDecl.value.type === "unparsed" && - property.startsWith("--") === false) + value.type === "invalid" || + (value.type === "unparsed" && property.startsWith("--") === false) ) { - newStyles.push({ - ...styleDecl, - property: `--${property}`, - }); + styleMap.set(`--${property}`, value); } else { - newStyles.push({ - ...styleDecl, - property: camelCaseProperty(property), - }); + if (value.type === "keyword" && value.value === "unset") { + styleMap.set(property, { type: "guaranteedInvalid" }); + } else styleMap.set(property, value); } } - - return newStyles; + return styleMap; }; diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 96548ecd17d3..6f20b660f34e 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -78,10 +78,10 @@ export const $advancedStylesLonghands = computed( } } // In advanced mode we assume user knows the properties they need, so we don't need to show these. - // @todo we need to find a better place for them in any case + // @todo https://github.com/webstudio-is/webstudio/issues/4871 if (settings.stylePanelMode !== "advanced") { for (const initialProperty of initialProperties) { - advancedStyles.set(initialProperty, { type: "unset", value: "" }); + advancedStyles.set(initialProperty, { type: "guaranteedInvalid" }); } } @@ -89,6 +89,7 @@ export const $advancedStylesLonghands = computed( } ); +// @todo delete export const $advancedStylesShorthands = computed( [$advancedStylesLonghands], (advancedStylesLonghands) => { diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 1d4805b86f80..870aa785a221 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -449,7 +449,7 @@ export const CssValueInput = ({ const value = props.intermediateValue ?? props.value ?? initialValue; const valueRef = useRef(value); valueRef.current = value; - + console.log({ property, value }); // Used to show description const [highlightedValue, setHighlighedValue] = useState< StyleValue | undefined diff --git a/packages/css-data/src/index.ts b/packages/css-data/src/index.ts index ca4ec477b889..e10c76fe2b85 100644 --- a/packages/css-data/src/index.ts +++ b/packages/css-data/src/index.ts @@ -17,6 +17,7 @@ export * from "./property-parsers/index"; export * from "./parse-css-value"; export * from "./parse-css"; export * from "./shorthands"; +export { shorthandProperties } from "./__generated__/shorthand-properties"; export { parseTailwindToWebstudio } from "./tailwind-parser/parse"; diff --git a/packages/css-engine/src/core/index.ts b/packages/css-engine/src/core/index.ts index acbc45904a2a..a884ba89fe7e 100644 --- a/packages/css-engine/src/core/index.ts +++ b/packages/css-engine/src/core/index.ts @@ -6,7 +6,7 @@ export type { FontFaceRule, } from "./rules"; export { prefixStyles } from "./prefixer"; -export { mergeStyles, isShorthand } from "./merger"; +export { mergeStyles, isShorthand, supportedShorthands } from "./merger"; export { generateStyleMap } from "./rules"; export type { StyleSheetRegular } from "./style-sheet-regular"; export * from "./create-style-sheet"; diff --git a/packages/css-engine/src/core/merger.ts b/packages/css-engine/src/core/merger.ts index 8f22baba05f6..52b759d6b364 100644 --- a/packages/css-engine/src/core/merger.ts +++ b/packages/css-engine/src/core/merger.ts @@ -139,22 +139,48 @@ const mergeBackgroundPosition = (styleMap: StyleMap) => { } }; -const supportedShorthandProperties = new Set([ - "margin", - "padding", - "border", - "outline", - "border-top", - "border-right", - "border-bottom", - "border-left", - "white-space", - "text-wrap", - "background-position", +export const supportedShorthands = new Map([ + [ + "margin", + new Set(["margin-top", "margin-right", "margin-bottom", "margin-left"]), + ], + [ + "padding", + new Set(["padding-top", "padding-right", "padding-bottom", "padding-left"]), + ], + [ + "border", + new Set([ + "border-top-width", + "border-right-width", + "border-bottom-width", + "border-left-width", + "border-top-style", + "border-right-style", + "border-bottom-style", + "border-left-style", + "border-top-color", + "border-right-color", + "border-bottom-color", + "border-left-color", + "border-image-source", + "border-image-slice", + "border-image-width", + "border-image-outset", + "border-image-repeat", + ]), + ], + ["outline", new Set(["outline-color", "outline-style", "outline-width"])], + ["white-space", new Set(["white-space-collapse", "text-wrap-mode"])], + ["text-wrap", new Set(["text-wrap-mode", "text-wrap-style"])], + [ + "background-position", + new Set(["background-position-x", "background-position-y"]), + ], ]); export const isShorthand = (property: string) => { - return supportedShorthandProperties.has(hyphenateProperty(property)); + return supportedShorthands.has(hyphenateProperty(property)); }; export const mergeStyles = (styleMap: StyleMap) => { diff --git a/packages/css-engine/src/schema.ts b/packages/css-engine/src/schema.ts index 50a75bea99ee..e7cf1af485e7 100644 --- a/packages/css-engine/src/schema.ts +++ b/packages/css-engine/src/schema.ts @@ -107,6 +107,10 @@ export const InvalidValue = z.object({ }); export type InvalidValue = z.infer; +/** + * Use GuaranteedInvalidValue if you need a temp placeholder before user enters a value + * @deprecated + */ const UnsetValue = z.object({ type: z.literal("unset"), value: z.literal(""), From 70e182901de429e7b7526d833914aec1a99b8263 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 25 Feb 2025 17:59:34 +0000 Subject: [PATCH 09/15] wip --- .../sections/advanced/advanced.tsx | 180 ++++-------------- .../sections/advanced/copy-paste-menu.tsx | 11 +- .../sections/advanced/parse-style-input.ts | 11 +- .../style-panel/sections/advanced/stores.ts | 16 -- .../css-value-input/css-value-input.tsx | 1 - packages/css-engine/src/core/index.ts | 2 +- packages/css-engine/src/core/merger.ts | 45 ----- 7 files changed, 46 insertions(+), 220 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 08eb43f55db6..2cb9181fa12c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -15,9 +15,7 @@ import { matchSorter } from "match-sorter"; import { PlusIcon } from "@webstudio-is/icons"; import { Box, - Collapsible, Flex, - focusRingStyle, Label, SearchField, SectionTitle, @@ -30,16 +28,11 @@ import { } from "@webstudio-is/design-system"; import { camelCaseProperty, - expandShorthands, - parseCssValue, propertyDescriptions, } from "@webstudio-is/css-data"; import { hyphenateProperty, - isShorthand, - mergeStyles, StyleValue, - supportedShorthands, toValue, type CssProperty, } from "@webstudio-is/css-engine"; @@ -64,7 +57,7 @@ import { PropertyInfo } from "../../property-label"; import { ColorPopover } from "../../shared/color-picker"; import { useClientSupports } from "~/shared/client-supports"; import { CopyPasteMenu, copyAttribute } from "./copy-paste-menu"; -import { $advancedStylesLonghands, $advancedStylesShorthands } from "./stores"; +import { $advancedStylesLonghands } from "./stores"; import { $settings } from "~/builder/shared/client-settings"; import { AddStyleInput } from "./add-style-input"; import { parseStyleInput } from "./parse-style-input"; @@ -379,94 +372,6 @@ const AdvancedDeclarationLonghand = memo( } ); -const AdvancedDeclarationShorthand = memo( - (props: { - property: CssProperty; - value: StyleValue | undefined; - autoFocus?: boolean; - onReset?: () => void; - onChangeComplete?: ComponentProps< - typeof CssValueInputContainer - >["onChangeComplete"]; - valueInputRef?: RefObject; - }) => { - const { property, value, onReset } = props; - const [isOpen, setIsOpen] = useState(false); - const longhands = expandShorthands([ - [hyphenateProperty(property), toValue(value)], - ]); - const camelCasedProperty = camelCaseProperty(property); - console.log(111, property, value, toValue(value)); - return ( - - - - - - {isOpen ? ": ▼" : ": ▶"} - - {toValue(value)} - - - - - {longhands.map(([property, value]) => { - return ( - - ); - })} - - - ); - } -); - -const toShorthands = (properties: Array) => { - console.log("properties", properties); - const shorthands = new Set(); - for (const property of properties) { - let isAdded = false; - for (const [shorthand, longhands] of supportedShorthands) { - if (longhands.has(property)) { - shorthands.add(shorthand); - isAdded = true; - break; - } - } - if (isAdded === false) { - shorthands.add(property); - } - } - console.log("shorthands", shorthands); - return Array.from(shorthands); -}; - export const Section = () => { const [isAdding, setIsAdding] = useState(false); const advancedStyles = useStore($advancedStylesLonghands); @@ -508,7 +413,7 @@ export const Section = () => { const newRecentPropertiesMap = new Map(recentPropertiesMap); newRecentPropertiesMap.set( selectedInstanceKey, - Array.from(new Set([...recentProperties, ...properties])) + Array.from(new Set(properties)) ); setRecentPropertiesMap(newRecentPropertiesMap); }; @@ -518,7 +423,7 @@ export const Section = () => { const insertedProperties = Array.from( styleMap.keys() ) as Array; - updateRecentProperties(insertedProperties); + updateRecentProperties([...recentProperties, ...insertedProperties]); return styleMap; }; @@ -564,7 +469,6 @@ export const Section = () => { }); }; - console.log({ recentProperties, currentProperties }); return ( { css={{ paddingInline: theme.panel.paddingInline, gap: 2 }} > {showRecentProperties && - toShorthands(recentProperties).map( - (property, index, properties) => { - const isLast = index === properties.length - 1; - const AdvancedDeclaration = isShorthand(property) - ? AdvancedDeclarationShorthand - : AdvancedDeclarationLonghand; - return ( - { - if (event.type === "enter") { - handleShowAddStyleInput(); - } - }} - onReset={() => { - updateRecentProperties( - recentProperties.filter( - (recentProperty) => recentProperty !== property - ) - ); - }} - /> - ); - } - )} + recentProperties.map((property, index, properties) => { + const isLast = index === properties.length - 1; + return ( + { + if (event.type === "enter") { + handleShowAddStyleInput(); + } + }} + onReset={() => { + updateRecentProperties( + recentProperties.filter( + (recentProperty) => recentProperty !== property + ) + ); + }} + /> + ); + })} {(showRecentProperties || isAdding) && ( { style={{ minHeight }} ref={containerRef} > - {toShorthands( - currentProperties.filter( + {currentProperties + .filter( (property) => recentProperties.includes(property) === false ) - ).map((property) => { - const AdvancedDeclaration = isShorthand(property) - ? AdvancedDeclarationShorthand - : AdvancedDeclarationLonghand; - return ( - - - - ); - })} + .map((property) => { + return ( + + + + ); + })} diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx index 94f875c3d578..b449200940e7 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/copy-paste-menu.tsx @@ -14,7 +14,7 @@ import { type StyleMap, } from "@webstudio-is/css-engine"; import { useStore } from "@nanostores/react"; -import { $advancedStylesShorthands, $advancedStylesLonghands } from "./stores"; +import { $advancedStylesLonghands } from "./stores"; export const copyAttribute = "data-declaration"; @@ -27,7 +27,6 @@ export const CopyPasteMenu = ({ properties: Array; onPaste: (cssText: string) => void; }) => { - const advancedStylesShorthands = useStore($advancedStylesShorthands); const advancedStylesLonghands = useStore($advancedStylesLonghands); const lastClickedProperty = useRef(); @@ -39,7 +38,7 @@ export const CopyPasteMenu = ({ // We want to only copy properties that are currently in front of the user. // That includes search or any future filters. const currentStyleMap: StyleMap = new Map(); - for (const [property, value] of advancedStylesShorthands) { + for (const [property, value] of advancedStylesLonghands) { const isEmpty = toValue(value) === ""; if (properties.includes(property) && isEmpty === false) { currentStyleMap.set(hyphenateProperty(property), value); @@ -52,13 +51,11 @@ export const CopyPasteMenu = ({ const handleCopy = () => { const property = lastClickedProperty.current; - console.log(property); + if (property === undefined) { return; } - const value = - advancedStylesShorthands.get(property) ?? - advancedStylesLonghands.get(property); + const value = advancedStylesLonghands.get(property); if (value === undefined) { return; diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts index ce7f0f679f53..fb769ae78581 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts @@ -4,11 +4,7 @@ import { camelCaseProperty, shorthandProperties, } from "@webstudio-is/css-data"; -import { - mergeStyles, - type CssProperty, - type StyleMap, -} from "@webstudio-is/css-engine"; +import { type CssProperty, type StyleMap } from "@webstudio-is/css-engine"; import { lexer } from "css-tree"; // When user provides only a property name, we need to make it `property:;` to be able to parse it. @@ -63,9 +59,8 @@ export const parseStyleInput = (css: string): StyleMap => { ) { styleMap.set(`--${property}`, value); } else { - if (value.type === "keyword" && value.value === "unset") { - styleMap.set(property, { type: "guaranteedInvalid" }); - } else styleMap.set(property, value); + // @todo This should be returning { type: "guaranteedInvalid" } + styleMap.set(property, value); } } return styleMap; diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts index 6f20b660f34e..c49cf28d5ae8 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts @@ -1,7 +1,6 @@ import { computed } from "nanostores"; import { hyphenateProperty, - mergeStyles, type CssProperty, type StyleMap, } from "@webstudio-is/css-engine"; @@ -88,18 +87,3 @@ export const $advancedStylesLonghands = computed( return advancedStyles; } ); - -// @todo delete -export const $advancedStylesShorthands = computed( - [$advancedStylesLonghands], - (advancedStylesLonghands) => { - const shorthandsMap: StyleMap = new Map(); - const shorthands = mergeStyles(advancedStylesLonghands); - - for (const [property, value] of shorthands) { - shorthandsMap.set(property, value); - } - - return shorthandsMap; - } -); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 870aa785a221..718551d871e0 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -449,7 +449,6 @@ export const CssValueInput = ({ const value = props.intermediateValue ?? props.value ?? initialValue; const valueRef = useRef(value); valueRef.current = value; - console.log({ property, value }); // Used to show description const [highlightedValue, setHighlighedValue] = useState< StyleValue | undefined diff --git a/packages/css-engine/src/core/index.ts b/packages/css-engine/src/core/index.ts index a884ba89fe7e..222d4023ff02 100644 --- a/packages/css-engine/src/core/index.ts +++ b/packages/css-engine/src/core/index.ts @@ -6,7 +6,7 @@ export type { FontFaceRule, } from "./rules"; export { prefixStyles } from "./prefixer"; -export { mergeStyles, isShorthand, supportedShorthands } from "./merger"; +export { mergeStyles } from "./merger"; export { generateStyleMap } from "./rules"; export type { StyleSheetRegular } from "./style-sheet-regular"; export * from "./create-style-sheet"; diff --git a/packages/css-engine/src/core/merger.ts b/packages/css-engine/src/core/merger.ts index 52b759d6b364..bb470d7e812e 100644 --- a/packages/css-engine/src/core/merger.ts +++ b/packages/css-engine/src/core/merger.ts @@ -2,7 +2,6 @@ import { StyleValue, TupleValue, TupleValueItem } from "../schema"; import { cssWideKeywords } from "../css"; import type { StyleMap } from "./rules"; import { toValue } from "./to-value"; -import { hyphenateProperty } from "./to-property"; /** * Css wide keywords cannot be used in shorthand parts @@ -139,50 +138,6 @@ const mergeBackgroundPosition = (styleMap: StyleMap) => { } }; -export const supportedShorthands = new Map([ - [ - "margin", - new Set(["margin-top", "margin-right", "margin-bottom", "margin-left"]), - ], - [ - "padding", - new Set(["padding-top", "padding-right", "padding-bottom", "padding-left"]), - ], - [ - "border", - new Set([ - "border-top-width", - "border-right-width", - "border-bottom-width", - "border-left-width", - "border-top-style", - "border-right-style", - "border-bottom-style", - "border-left-style", - "border-top-color", - "border-right-color", - "border-bottom-color", - "border-left-color", - "border-image-source", - "border-image-slice", - "border-image-width", - "border-image-outset", - "border-image-repeat", - ]), - ], - ["outline", new Set(["outline-color", "outline-style", "outline-width"])], - ["white-space", new Set(["white-space-collapse", "text-wrap-mode"])], - ["text-wrap", new Set(["text-wrap-mode", "text-wrap-style"])], - [ - "background-position", - new Set(["background-position-x", "background-position-y"]), - ], -]); - -export const isShorthand = (property: string) => { - return supportedShorthands.has(hyphenateProperty(property)); -}; - export const mergeStyles = (styleMap: StyleMap) => { const newStyle = new Map(styleMap); mergeBorder(newStyle, "border-top"); From 32ff768e77947db3e514b7748a9becf569fbbeeb Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 25 Feb 2025 19:35:33 +0000 Subject: [PATCH 10/15] remove autofocus --- .../sections/advanced/add-style-input.tsx | 2 -- .../sections/advanced/advanced.tsx | 33 ++++++------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx index 6cc8b6410726..95e0d32822f6 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx @@ -242,7 +242,6 @@ export const AddStyleInput = forwardRef< (null); - useEffect(() => { - if (autoFocus) { - inputRef.current?.focus(); - inputRef.current?.select(); - } - }, [autoFocus]); const isColor = colord(toValue(styleDecl.usedValue)).isValid(); return ( @@ -325,15 +317,12 @@ const LazyRender = ({ children }: ComponentProps<"div">) => { const AdvancedDeclarationLonghand = memo( ({ property, - autoFocus, onChangeComplete, onReset, valueInputRef, indentation = initialIndentation, }: { property: CssProperty; - value: StyleValue | undefined; - autoFocus?: boolean; indentation?: string; onReset?: () => void; onChangeComplete?: ComponentProps< @@ -361,7 +350,6 @@ const AdvancedDeclarationLonghand = memo( : { }; const handleShowAddStyleInput = () => { - setIsAdding(true); + flushSync(() => { + setIsAdding(true); + }); // User can click twice on the add button, so we need to focus the input on the second click after autoFocus isn't working. addPropertyInputRef.current?.focus(); }; @@ -460,12 +450,13 @@ export const Section = () => { setSearchProperties(matched as CssProperty[]); }; - const handleAbortAddStyles = () => { + const afterAddingStyles = () => { setIsAdding(false); requestAnimationFrame(() => { // We are either focusing the last value input from the recent list if available or the search input. const element = recentValueInputRef.current ?? searchInputRef.current; element?.focus(); + element?.select(); }); }; @@ -496,10 +487,9 @@ export const Section = () => { const isLast = index === properties.length - 1; return ( { if (event.type === "enter") { handleShowAddStyleInput(); @@ -528,10 +518,10 @@ export const Section = () => { onSubmit={(cssText: string) => { const styles = handleInsertStyles(cssText); if (styles.size > 0) { - setIsAdding(false); + afterAddingStyles(); } }} - onClose={handleAbortAddStyles} + onClose={afterAddingStyles} onFocus={() => { if (isAdding === false) { handleShowAddStyleInput(); @@ -559,10 +549,7 @@ export const Section = () => { .map((property) => { return ( - + ); })} From cbf7163ea38faa0aac18de4380353a4d18dbffc7 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 25 Feb 2025 19:45:22 +0000 Subject: [PATCH 11/15] add a test for later --- packages/css-data/src/parse-css.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/css-data/src/parse-css.test.ts b/packages/css-data/src/parse-css.test.ts index edd206b34fba..708fd392e67d 100644 --- a/packages/css-data/src/parse-css.test.ts +++ b/packages/css-data/src/parse-css.test.ts @@ -22,6 +22,17 @@ describe("Parse CSS", () => { ]); }); + // @todo this is wrong + test.skip("parse declaration with missing value", () => { + expect(parseCss(`.test { color:;}`)).toEqual([ + { + selector: ".test", + property: "color", + value: { type: "guaranteedInvalid" }, + }, + ]); + }); + test("parse supported shorthand values", () => { const css = ` .test { From 8efd1bf287748f1aa58c1d643378d8863d3fbe9f Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 25 Feb 2025 20:01:22 +0000 Subject: [PATCH 12/15] add descriptions --- .../bin/property-value-descriptions.ts | 10 ++- .../property-value-descriptions.ts | 80 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/packages/css-data/bin/property-value-descriptions.ts b/packages/css-data/bin/property-value-descriptions.ts index f980b3e047f8..faab843505b7 100755 --- a/packages/css-data/bin/property-value-descriptions.ts +++ b/packages/css-data/bin/property-value-descriptions.ts @@ -1,10 +1,11 @@ /* eslint-disable func-style */ import * as fs from "node:fs"; import * as path from "node:path"; -import type { CreateChatCompletionResponse } from "openai"; -import { keywordValues } from "../src/__generated__/keyword-values"; import warnOnce from "warn-once"; import pRetry from "p-retry"; +import type { CreateChatCompletionResponse } from "openai"; +import { keywordValues } from "../src/__generated__/keyword-values"; +import { shorthandProperties } from "../src/__generated__/shorthand-properties"; import { customLonghandPropertyNames } from "../src/custom-data"; const propertiesPrompt = fs.readFileSync( @@ -66,7 +67,10 @@ const batchSize = 16; /** * Properties descriptions */ -const newPropertiesNames = Object.keys(keywordValues) +const newPropertiesNames = [ + ...Object.keys(keywordValues), + ...shorthandProperties, +] // Slice to generate only X - useful for testing. // .slice(0, 30) .filter( diff --git a/packages/css-data/src/__generated__/property-value-descriptions.ts b/packages/css-data/src/__generated__/property-value-descriptions.ts index b13676ec708c..3f5c22cff54a 100644 --- a/packages/css-data/src/__generated__/property-value-descriptions.ts +++ b/packages/css-data/src/__generated__/property-value-descriptions.ts @@ -505,6 +505,86 @@ export const propertiesGenerated = { fieldSizing: "Controls the algorithm used to calculate the width of form controls.", zoom: "Specifies the zoom level of a document.", + animation: + "Animations make it possible to animate transitions from one CSS style configuration to another.", + background: + "The background property is a shorthand for setting multiple background properties at once.", + border: + "Borders outline the border area of an element and are used for decoration and visual separation.", + "border-block": + "The border-block property is a shorthand for setting the individual properties for the vertical block axis borders.", + "border-block-end": + "The border-block-end property is a shorthand for setting the individual properties for the end block axis border.", + "border-block-start": + "The border-block-start property is a shorthand for setting the individual properties for the start block axis border.", + "border-bottom": + "The border-bottom property is a shorthand for setting the individual properties for the bottom border.", + "border-color": + "The border-color property sets the color of the four borders around an element.", + "border-image": + "The border-image property is a shorthand for setting the border-image-source, border-image-slice, border-image-width, and border-image-repeat properties.", + "border-inline": + "The border-inline property is a shorthand for setting the individual properties for the inline axis borders.", + "border-inline-end": + "The border-inline-end property is a shorthand for setting the individual properties for the end inline axis border.", + "border-inline-start": + "The border-inline-start property is a shorthand for setting the individual properties for the start inline axis border.", + "border-left": + "The border-left property is a shorthand for setting the individual properties for the left border.", + "border-radius": + "Border-radius is used to create rounded corners on an element's box.", + "border-right": + "The border-right property is a shorthand for setting the individual properties for the right border.", + "border-style": + "The border-style property sets the style of the four borders.", + "border-top": "Controls the top border of an element.", + "border-width": "Sets the width of the borders of an element.", + "column-rule": "Manages the line drawn between columns.", + columns: "Specifies the number and width of columns.", + "contain-intrinsic-size": + "Defines the size of an element's intrinsic content.", + container: "Establishes a new block formatting context.", + flex: "Specifies the length of a flexible item.", + "flex-flow": "Sets how flex items are placed in the flex container.", + font: "Specifies font styles for text.", + gap: "Sets the gap between grid items.", + grid: "Defines a grid or a subgrid.", + "grid-area": "Specifies the size of a grid item.", + "grid-column": "Specifies a grid item's position within the grid.", + "grid-row": "Specifies a grid item's row within the grid.", + "grid-template": "Sets the values for grid layout properties.", + inset: "Specifies the position of an element.", + "inset-block": + "Controls the block axis position of an absolutely positioned element.", + "inset-inline": + "Controls the inline axis position of an absolutely positioned element.", + "list-style": "Sets all the properties for a list in one declaration.", + margin: "Sets the margin on all four sides of an element at once.", + "margin-block": "Sets the margin on the block axis of an element.", + "margin-inline": "Sets the margin on the inline axis of an element.", + mask: "Lets you use an image or a CSS gradient to define the shape of the mask.", + "mask-border": + "Lets you use an image or a CSS gradient to define the border image of the mask.", + offset: "Controls the positioning of a positioned element.", + outline: "Sets the style of the outline around an element.", + padding: "Sets the padding on all four sides of an element at once.", + "padding-block": "Sets the padding on the block axis of an element.", + "padding-inline": "Sets the padding on the inline axis of an element.", + "place-content": "Aligns a flex container's lines within it.", + "place-items": "Aligns a flex container's items within it.", + "place-self": "Allows the default alignment of a flex item to be overridden.", + "scroll-margin": "Controls the top and bottom sides of the scrollbar", + "scroll-margin-block": "Controls the top and bottom sides of the scrollbar", + "scroll-margin-inline": "Controls the left and right sides of the scrollbar", + "scroll-padding": + "Sets the amount of space between the element's content and its padding", + "scroll-padding-block": + "Sets the amount of space between the element's content and its top and bottom padding", + "scroll-padding-inline": + "Sets the amount of space between the element's content and its left and right padding", + "text-decoration": + "Adds decoration to text, such as underlines or line-through", + "text-emphasis": "Sets special emphasis on text, like dots or circles", } as Record; export const propertiesOverrides = {} as Record; From 6e643f3969a3f1830791c25ed54257f29c5f012c Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 26 Feb 2025 10:10:33 +0000 Subject: [PATCH 13/15] cleanup --- .../features/style-panel/sections/advanced/advanced.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 5a0d3212b2f1..2860e3daf4eb 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -306,10 +306,7 @@ const LazyRender = ({ children }: ComponentProps<"div">) => { containIntrinsicSize: "auto 44px", }} > - { - //children - isVisible ? children : undefined - } + {isVisible ? children : undefined} ); }; From b5319ece507f529c2f21e2e20408f2a3cf249544 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 26 Feb 2025 14:44:10 +0000 Subject: [PATCH 14/15] allow searching recent --- .../sections/advanced/advanced.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx index 2860e3daf4eb..3f6575f435e0 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx @@ -378,12 +378,16 @@ export const Section = () => { advancedStyles.keys() ) as Array; - const currentProperties = searchProperties ?? advancedProperties; - const recentProperties = selectedInstanceKey ? (recentPropertiesMap.get(selectedInstanceKey) ?? []) : []; + const currentProperties = + searchProperties ?? + advancedProperties.filter( + (property) => recentProperties.includes(property) === false + ); + const showRecentProperties = recentProperties.length > 0 && searchProperties === undefined; @@ -539,17 +543,13 @@ export const Section = () => { style={{ minHeight }} ref={containerRef} > - {currentProperties - .filter( - (property) => recentProperties.includes(property) === false - ) - .map((property) => { - return ( - - - - ); - })} + {currentProperties.map((property) => { + return ( + + + + ); + })} From aa579bea5cd556392d3c63802e97620108fb386a Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 26 Feb 2025 15:29:41 +0000 Subject: [PATCH 15/15] fix insertion in camel case instead of hyphentaed --- .../sections/advanced/add-style-input.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx index 95e0d32822f6..276eb2e881ec 100644 --- a/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/advanced/add-style-input.tsx @@ -51,9 +51,10 @@ const getAutocompleteItems = () => { return autoCompleteItems; } for (const property in propertiesData) { + const hyphenatedProperty = hyphenateProperty(property); autoCompleteItems.push({ - property, - label: hyphenateProperty(property), + property: hyphenatedProperty, + label: hyphenatedProperty, }); } @@ -72,10 +73,11 @@ const getAutocompleteItems = () => { if (ignoreValues.has(value)) { continue; } + const hyphenatedProperty = hyphenateProperty(property); autoCompleteItems.push({ - property, + property: hyphenatedProperty, value, - label: `${hyphenateProperty(property)}: ${value}`, + label: `${hyphenatedProperty}: ${value}`, }); } } @@ -114,7 +116,7 @@ const matchOrSuggestToCreate = ( // Now we will suggest to insert each longhand separately. for (const [property, value] of styleMap) { matched.push({ - property: property, + property, value: toValue(value), label: `Create "${generateStyleMap(new Map([[property, value]]))}"`, });