diff --git a/apps/builder/app/builder/features/style-panel/controls/color/color-control.tsx b/apps/builder/app/builder/features/style-panel/controls/color/color-control.tsx index 04f6d3d144..d9eed291c8 100644 --- a/apps/builder/app/builder/features/style-panel/controls/color/color-control.tsx +++ b/apps/builder/app/builder/features/style-panel/controls/color/color-control.tsx @@ -1,12 +1,14 @@ +import { useStore } from "@nanostores/react"; import type { StyleProperty } from "@webstudio-is/css-engine"; import { ColorPicker } from "../../shared/color-picker"; import { styleConfigByName } from "../../shared/configs"; -import { useComputedStyleDecl } from "../../shared/model"; +import { $availableVariables, useComputedStyleDecl } from "../../shared/model"; import { deleteProperty, setProperty } from "../../shared/use-style-data"; export const ColorControl = ({ property }: { property: StyleProperty }) => { const computedStyleDecl = useComputedStyleDecl(property); const { items } = styleConfigByName(property); + const availableVariables = useStore($availableVariables); const value = computedStyleDecl.cascadedValue; const currentColor = computedStyleDecl.usedValue; const setValue = setProperty(property); @@ -15,7 +17,13 @@ export const ColorControl = ({ property }: { property: StyleProperty }) => { property={property} value={value} currentColor={currentColor} - keywords={items.map((item) => ({ type: "keyword", value: item.name }))} + options={[ + ...items.map((item) => ({ + type: "keyword" as const, + value: item.name, + })), + ...availableVariables, + ]} onChange={(styleValue) => setValue(styleValue, { isEphemeral: true })} onChangeComplete={setValue} onAbort={() => deleteProperty(property, { isEphemeral: true })} diff --git a/apps/builder/app/builder/features/style-panel/controls/position/position-control.tsx b/apps/builder/app/builder/features/style-panel/controls/position/position-control.tsx index 6dbff953db..e3672c60cc 100644 --- a/apps/builder/app/builder/features/style-panel/controls/position/position-control.tsx +++ b/apps/builder/app/builder/features/style-panel/controls/position/position-control.tsx @@ -110,7 +110,7 @@ export const PositionControl = ({ { const computedStyleDecl = useComputedStyleDecl(property); @@ -16,13 +17,20 @@ export const TextControl = ({ property }: { property: StyleProperty }) => { StyleValue | IntermediateStyleValue >(); const items = styleConfigByName(property).items; + const availableVariables = useStore($availableVariables); return ( ({ type: "keyword", value: item.name }))} + options={[ + ...items.map((item) => ({ + type: "keyword" as const, + value: item.name, + })), + ...availableVariables, + ]} onChange={(styleValue) => { setIntermediateValue(styleValue); if (styleValue === undefined) { 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 741b2a6735..d5914a72e9 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 @@ -35,7 +35,7 @@ import { CssValueInputContainer } from "../../shared/css-value-input"; import { styleConfigByName } from "../../shared/configs"; import { deleteProperty, setProperty } from "../../shared/use-style-data"; import { - $definedStyles, + $availableVariables, $matchingBreakpoints, getDefinedStyles, useComputedStyleDecl, @@ -210,16 +210,6 @@ const AdvancedPropertyLabel = ({ property }: { property: StyleProperty }) => { ); }; -const $availableCustomProperties = computed($definedStyles, (definedStyles) => { - const customProperties = new Set(); - for (const { property } of definedStyles) { - if (property.startsWith("--")) { - customProperties.add(property); - } - } - return customProperties; -}); - const AdvancedPropertyValue = ({ autoFocus, property, @@ -228,7 +218,7 @@ const AdvancedPropertyValue = ({ property: StyleProperty; }) => { const styleDecl = useComputedStyleDecl(property); - const availableCustomProperties = useStore($availableCustomProperties); + const availableVariables = useStore($availableVariables); const { items } = styleConfigByName(property); const inputRef = useRef(null); useEffect(() => { @@ -265,16 +255,12 @@ const AdvancedPropertyValue = ({ } property={property} styleSource={styleDecl.source.name} - keywords={[ + options={[ ...items.map((item) => ({ type: "keyword" as const, value: item.name, })), - // very basic custom properties autocomplete - ...Array.from(availableCustomProperties).map((name) => ({ - type: "keyword" as const, - value: name, - })), + ...availableVariables, ]} value={styleDecl.cascadedValue} setValue={(styleValue, options) => { diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-position.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-position.tsx index 3b0cbea300..bf2b3489da 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-position.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/background-position.tsx @@ -73,7 +73,7 @@ export const BackgroundPosition = ({ index }: { index: number }) => { { { disabled={customSizeDisabled} property={property} styleSource="default" - keywords={customSizeOptions} + options={customSizeOptions} value={customSizeValue.value[0]} setValue={setValueX} deleteProperty={() => {}} @@ -156,7 +156,7 @@ export const BackgroundSize = ({ index }: { index: number }) => { disabled={customSizeDisabled} property={property} styleSource="default" - keywords={customSizeOptions} + options={customSizeOptions} value={customSizeValue.value[1]} setValue={setValueY} deleteProperty={() => {}} diff --git a/apps/builder/app/builder/features/style-panel/sections/borders/border-color.tsx b/apps/builder/app/builder/features/style-panel/sections/borders/border-color.tsx index 446f7a6239..2dbf7dbeb2 100644 --- a/apps/builder/app/builder/features/style-panel/sections/borders/border-color.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/borders/border-color.tsx @@ -58,7 +58,7 @@ export const BorderColor = () => { currentColor={currentColor} property={local.property as StyleProperty} value={value} - keywords={items.map((item) => ({ + options={items.map((item) => ({ type: "keyword", value: item.name, }))} diff --git a/apps/builder/app/builder/features/style-panel/sections/borders/border-property.tsx b/apps/builder/app/builder/features/style-panel/sections/borders/border-property.tsx index a586ca6bad..6d26cb35dc 100644 --- a/apps/builder/app/builder/features/style-panel/sections/borders/border-property.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/borders/border-property.tsx @@ -85,7 +85,7 @@ export const BorderProperty = ({ { const batch = createBatchUpdate(); @@ -123,7 +123,7 @@ export const BorderProperty = ({ } property={styleDecl.property as StyleProperty} styleSource={styleDecl.source.name} - keywords={keywords} + options={keywords} value={styleDecl.cascadedValue} setValue={setProperty(styleDecl.property as StyleProperty)} deleteProperty={deleteProperty} diff --git a/apps/builder/app/builder/features/style-panel/sections/layout/layout.tsx b/apps/builder/app/builder/features/style-panel/sections/layout/layout.tsx index 9556d477f2..c675a2905b 100644 --- a/apps/builder/app/builder/features/style-panel/sections/layout/layout.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/layout/layout.tsx @@ -152,7 +152,7 @@ const GapInput = ({ property={property} value={styleDecl.cascadedValue} intermediateValue={intermediateValue} - keywords={items.map((item) => ({ + options={items.map((item) => ({ type: "keyword", value: item.name, }))} diff --git a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-and-perspective-origin.tsx b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-and-perspective-origin.tsx index 725f760f1b..46c98cb31c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-and-perspective-origin.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-and-perspective-origin.tsx @@ -159,7 +159,7 @@ export const TransformAndPerspectiveOrigin = ( /> {}} @@ -186,7 +186,7 @@ export const TransformAndPerspectiveOrigin = ( /> {}} @@ -208,7 +208,6 @@ export const TransformAndPerspectiveOrigin = ( /> {}} diff --git a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-rotate.tsx b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-rotate.tsx index 557bc26810..1e0b381749 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-rotate.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-rotate.tsx @@ -53,7 +53,6 @@ export const RotatePanelContent = () => { styleSource="local" property="rotate" value={rotateX} - keywords={[]} setValue={(value, options) => updateTransformFunction(styleDecl, "rotateX", value, options) } @@ -73,7 +72,6 @@ export const RotatePanelContent = () => { styleSource="local" property="rotate" value={rotateY} - keywords={[]} setValue={(value, options) => updateTransformFunction(styleDecl, "rotateY", value, options) } @@ -93,7 +91,6 @@ export const RotatePanelContent = () => { styleSource="local" property="rotate" value={rotateZ} - keywords={[]} setValue={(value, options) => updateTransformFunction(styleDecl, "rotateZ", value, options) } diff --git a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-scale.tsx b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-scale.tsx index cdc2bb0df8..0db73b841b 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-scale.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-scale.tsx @@ -97,7 +97,6 @@ export const ScalePanelContent = () => { { @@ -138,7 +137,6 @@ export const ScalePanelContent = () => { { @@ -179,7 +177,6 @@ export const ScalePanelContent = () => { { diff --git a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-skew.tsx b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-skew.tsx index 567193880e..1abb726d31 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-skew.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-skew.tsx @@ -50,7 +50,6 @@ export const SkewPanelContent = () => { styleSource="local" property={fakeProperty} value={skewX} - keywords={[]} setValue={(value, options) => updateTransformFunction(styleDecl, "skewX", value, options) } @@ -70,7 +69,6 @@ export const SkewPanelContent = () => { styleSource="local" property={fakeProperty} value={skewY} - keywords={[]} setValue={(value, options) => updateTransformFunction(styleDecl, "skewY", value, options) } diff --git a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-translate.tsx b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-translate.tsx index 851be6b2cd..fa988de990 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transforms/transform-translate.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transforms/transform-translate.tsx @@ -59,7 +59,6 @@ export const TranslatePanelContent = () => { styleSource="local" property={property} value={translateX} - keywords={[]} setValue={(newValue, options) => setAxis(0, newValue, options)} deleteProperty={() => {}} /> @@ -77,7 +76,6 @@ export const TranslatePanelContent = () => { styleSource="local" property={property} value={translateY} - keywords={[]} setValue={(newValue, options) => setAxis(1, newValue, options)} deleteProperty={() => {}} /> @@ -95,7 +93,6 @@ export const TranslatePanelContent = () => { styleSource="local" property={property} value={translateZ} - keywords={[]} setValue={(newValue, options) => setAxis(2, newValue, options)} deleteProperty={() => {}} /> diff --git a/apps/builder/app/builder/features/style-panel/sections/transitions/transition-content.tsx b/apps/builder/app/builder/features/style-panel/sections/transitions/transition-content.tsx index 87c62dd655..963580ac3c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/transitions/transition-content.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/transitions/transition-content.tsx @@ -147,7 +147,6 @@ export const TransitionContent = ({ index }: { index: number }) => { property={"transitionDuration"} styleSource="local" value={duration ?? properties.transitionDuration.initial} - keywords={[]} deleteProperty={() => {}} setValue={(value, options) => { if (value === undefined) { @@ -175,7 +174,6 @@ export const TransitionContent = ({ index }: { index: number }) => { key={"transitionDelay"} styleSource="local" value={delay ?? properties.transitionDelay.initial} - keywords={[]} deleteProperty={() => {}} setValue={(value, options) => { if (value === undefined) { diff --git a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx index dd1bfa3ae8..8e67d37ac6 100644 --- a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx @@ -9,6 +9,7 @@ import type { StyleValue, KeywordValue, RgbValue, + VarValue, } from "@webstudio-is/css-engine"; import { Popover, @@ -171,7 +172,7 @@ type ColorPickerProps = { onAbort: () => void; value: StyleValue; currentColor: StyleValue; - keywords?: Array; + options?: Array; property: StyleProperty; disabled?: boolean; }; @@ -239,7 +240,7 @@ export const ColorPopover = ({ export const ColorPicker = ({ value, currentColor, - keywords, + options, property, disabled, onChange, @@ -275,7 +276,7 @@ export const ColorPicker = ({ property={property} value={value} intermediateValue={intermediateValue} - keywords={keywords} + options={options} onChange={(styleValue) => { if (styleValue === undefined) { setIntermediateValue(styleValue); @@ -289,6 +290,7 @@ export const ColorPicker = ({ if ( styleValue.type === "rgb" || styleValue.type === "keyword" || + styleValue.type === "var" || styleValue.type === "invalid" ) { setIntermediateValue(styleValue); @@ -309,7 +311,11 @@ export const ColorPicker = ({ } }} onChangeComplete={({ value }) => { - if (value.type === "rgb" || value.type === "keyword") { + if ( + value.type === "rgb" || + value.type === "keyword" || + value.type === "var" + ) { setIntermediateValue(undefined); onChangeComplete(value); return; diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx index 1a217f9d6e..6ec1202791 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx @@ -17,7 +17,7 @@ type CssValueInputContainerProps = { export const CssValueInputContainer = ({ property, - keywords, + options, setValue, deleteProperty, ...props @@ -31,7 +31,7 @@ export const CssValueInputContainer = ({ {...props} property={property} intermediateValue={intermediateValue} - keywords={keywords} + options={options} onChange={(styleValue) => { setIntermediateValue(styleValue); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.stories.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.stories.tsx index 4fa4976fed..183ff33137 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.stories.tsx @@ -26,7 +26,7 @@ export const WithKeywords = () => { property="width" value={value} intermediateValue={intermediateValue} - keywords={[ + options={[ { type: "keyword", value: "auto" }, { type: "keyword", value: "min-content" }, { type: "keyword", value: "max-content" }, @@ -67,7 +67,7 @@ export const WithIcons = () => { property="alignItems" value={value} intermediateValue={intermediateValue} - keywords={[ + options={[ { type: "keyword", value: "normal" }, { type: "keyword", value: "start" }, { type: "keyword", value: "end" }, @@ -113,7 +113,7 @@ export const WithUnits = () => { property="rowGap" value={value} intermediateValue={intermediateValue} - keywords={[ + options={[ { type: "keyword", value: "auto" }, { type: "keyword", value: "min-content" }, { type: "keyword", value: "max-content" }, 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 1ae6614b93..05f9ec20d5 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 @@ -22,6 +22,7 @@ import type { StyleProperty, StyleValue, Unit, + VarValue, } from "@webstudio-is/css-engine"; import { type KeyboardEventHandler, @@ -310,7 +311,7 @@ type CssValueInputProps = Pick< /** * Selected item in the dropdown */ - keywords?: Array; + options?: Array; onChange: (value: CssValueInputValue | undefined) => void; onChangeComplete: (event: ChangeCompleteEvent) => void; onHighlight: (value: StyleValue | undefined) => void; @@ -327,7 +328,7 @@ const initialValue: IntermediateStyleValue = { const itemToString = (item: CssValueInputValue | null) => { return item === null ? "" - : item.type === "keyword" + : item.type === "keyword" || item.type === "var" ? // E.g. we want currentcolor to be lower case toValue(item).toLocaleLowerCase() : item.type === "intermediate" || item.type === "unit" @@ -356,7 +357,7 @@ const Description = styled(Box, { width: theme.spacing[27] }); * - Scrub interaction * - Click outside, unit selection or escape when list is open should unfocus the unit select trigger * - * Keywords mode: + * Options mode: * - When any character in the input is not a number we automatically switch to keywords mode on keydown * - Filterable keywords list (click on chevron or arrow down to show the list) * - Arrow keys are used to navigate keyword items @@ -375,7 +376,7 @@ export const CssValueInput = ({ showSuffix = true, styleSource, property, - keywords = [], + options = [], onHighlight, onAbort, disabled, @@ -445,7 +446,7 @@ export const CssValueInput = ({ highlightedIndex, } = useCombobox({ // Used for description to match the item when nothing is highlighted yet and value is still in non keyword mode - items: keywords, + items: options, value, selectedItem: props.value, itemToString, @@ -729,7 +730,9 @@ export const CssValueInput = ({ {...getItemProps({ item, index })} key={index} > - {itemToString(item)} + {item.type === "var" + ? `--${item.value}` + : itemToString(item)} ))} diff --git a/apps/builder/app/builder/features/style-panel/shared/filter-content.tsx b/apps/builder/app/builder/features/style-panel/shared/filter-content.tsx index eca7a67ecd..d7f2cbb353 100644 --- a/apps/builder/app/builder/features/style-panel/shared/filter-content.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/filter-content.tsx @@ -213,7 +213,6 @@ export const FilterSectionContent = ({ unit: "px", } } - keywords={[]} setValue={handleFilterFunctionValueChange} deleteProperty={() => {}} /> diff --git a/apps/builder/app/builder/features/style-panel/shared/model.tsx b/apps/builder/app/builder/features/style-panel/shared/model.tsx index 29a146384c..9bc64286fa 100644 --- a/apps/builder/app/builder/features/style-panel/shared/model.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/model.tsx @@ -6,6 +6,7 @@ import { compareMedia, type StyleProperty, type StyleValue, + type VarValue, } from "@webstudio-is/css-engine"; import { ROOT_INSTANCE_ID, @@ -165,6 +166,20 @@ export const $definedStyles = computed( } ); +export const $availableVariables = computed($definedStyles, (definedStyles) => { + const availableVariables = new Map(); + for (const { property } of definedStyles) { + if (property.startsWith("--")) { + // deduplicate by property name + availableVariables.set(property, { + type: "var", + value: property.slice(2), + }); + } + } + return Array.from(availableVariables.values()); +}); + const $model = computed( [ $styles, diff --git a/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx b/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx index e63830be4a..16cb300bbb 100644 --- a/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx @@ -214,7 +214,6 @@ export const ShadowContent = ({ // outline-offset is a fake property for validating box-shadow's offsetX. property="outlineOffset" styleSource="local" - keywords={[]} value={offsetX ?? { type: "unit", value: 0, unit: "px" }} setValue={(value, options) => handlePropertyChange({ offsetX: value }, options) @@ -238,7 +237,6 @@ export const ShadowContent = ({ // outline-offset is a fake property for validating box-shadow's offsetY. property="outlineOffset" styleSource="local" - keywords={[]} value={offsetY ?? { type: "unit", value: 0, unit: "px" }} setValue={(value, options) => handlePropertyChange({ offsetY: value }, options) @@ -262,7 +260,6 @@ export const ShadowContent = ({ // border-top-width is a fake property for validating box-shadow's blur. property="borderTopWidth" styleSource="local" - keywords={[]} value={blur ?? { type: "unit", value: 0, unit: "px" }} setValue={(value, options) => handlePropertyChange({ blur: value }, options) @@ -287,7 +284,6 @@ export const ShadowContent = ({ // outline-offset is a fake property for validating box-shadow's spread. property="outlineOffset" styleSource="local" - keywords={[]} value={spread ?? { type: "unit", value: 0, unit: "px" }} setValue={(value, options) => handlePropertyChange({ spread: value }, options) @@ -320,7 +316,7 @@ export const ShadowContent = ({ property="color" value={colorControlProp} currentColor={colorControlProp} - keywords={styleConfigByName("color").items.map((item) => ({ + options={styleConfigByName("color").items.map((item) => ({ type: "keyword", value: item.name, }))}