experimental: support css variables in dedicated sections (#4212)

Ref https://github.com/webstudio-is/webstudio/issues/3399

Added support for entering and autocomplete of css variables to
dedicated style sections like size and typography.

- color control
- text control

Also improved autocomplete to search with both var(--name) and --name.

![Screenshot 2024-10-04 at 01 51
12](https://github.com/user-attachments/assets/aca0f63d-8063-4676-877b-cc90e492ce9a)
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2024-10-05 01:41:35 +03:00
1 parent 5bfdd683b3
commit 63460bd2cb
22 files changed
+76 -69

No files matched your search

@@ -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 })}
@@ -110,7 +110,7 @@ export const PositionControl = ({
<CssValueInputContainer
property={property}
styleSource={styleDecl.source.name}
keywords={keywords}
options={keywords}
value={value.value[0]}
setValue={setValueX}
deleteProperty={deleteProperty}
@@ -123,7 +123,7 @@ export const PositionControl = ({
<CssValueInputContainer
property={property}
styleSource={styleDecl.source.name}
keywords={keywords}
options={keywords}
value={value.value[1]}
setValue={setValueY}
deleteProperty={deleteProperty}
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useStore } from "@nanostores/react";
import type { StyleProperty, StyleValue } from "@webstudio-is/css-engine";
import {
CssValueInput,
@@ -6,7 +7,7 @@ import {
} from "../../shared/css-value-input";
import { styleConfigByName } from "../../shared/configs";
import { deleteProperty, setProperty } from "../../shared/use-style-data";
import { useComputedStyleDecl } from "../../shared/model";
import { $availableVariables, useComputedStyleDecl } from "../../shared/model";
export const TextControl = ({ property }: { property: StyleProperty }) => {
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 (
<CssValueInput
styleSource={computedStyleDecl.source.name}
property={property}
value={value}
intermediateValue={intermediateValue}
keywords={items.map((item) => ({ type: "keyword", value: item.name }))}
options={[
...items.map((item) => ({
type: "keyword" as const,
value: item.name,
})),
...availableVariables,
]}
onChange={(styleValue) => {
setIntermediateValue(styleValue);
if (styleValue === undefined) {
@@ -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<StyleProperty>();
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<HTMLInputElement>(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) => {
@@ -73,7 +73,7 @@ export const BackgroundPosition = ({ index }: { index: number }) => {
<CssValueInputContainer
property="backgroundPositionX"
styleSource="default"
keywords={[
options={[
{ type: "keyword", value: "center" },
{ type: "keyword", value: "left" },
{ type: "keyword", value: "right" },
@@ -96,7 +96,7 @@ export const BackgroundPosition = ({ index }: { index: number }) => {
<CssValueInputContainer
property="backgroundPositionY"
styleSource="default"
keywords={[
options={[
{ type: "keyword", value: "center" },
{ type: "keyword", value: "top" },
{ type: "keyword", value: "bottom" },
@@ -146,7 +146,7 @@ export const BackgroundSize = ({ 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={() => {}}
@@ -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,
}))}
@@ -85,7 +85,7 @@ export const BorderProperty = ({
<CssValueInputContainer
property={firstPropertyName}
styleSource={styleValueSourceColor}
keywords={keywords}
options={keywords}
value={value}
setValue={(newValue, options) => {
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}
@@ -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,
}))}
@@ -159,7 +159,7 @@ export const TransformAndPerspectiveOrigin = (
/>
<CssValueInputContainer
value={origin.x}
keywords={xOriginKeywords}
options={xOriginKeywords}
styleSource="local"
property={fakePropertyX}
deleteProperty={() => {}}
@@ -186,7 +186,7 @@ export const TransformAndPerspectiveOrigin = (
/>
<CssValueInputContainer
value={origin.y}
keywords={yOriginKeywords}
options={yOriginKeywords}
styleSource="local"
property={fakePropertyY}
deleteProperty={() => {}}
@@ -208,7 +208,6 @@ export const TransformAndPerspectiveOrigin = (
/>
<CssValueInputContainer
value={origin.z}
keywords={[]}
styleSource="local"
property={fakePropertyZ}
deleteProperty={() => {}}
@@ -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)
}
@@ -97,7 +97,6 @@ export const ScalePanelContent = () => {
<CssValueInput
styleSource="local"
property={property}
keywords={[]}
value={scaleX}
intermediateValue={intermediateScalingX}
onChange={(value) => {
@@ -138,7 +137,6 @@ export const ScalePanelContent = () => {
<CssValueInput
styleSource="local"
property={property}
keywords={[]}
value={scaleY}
intermediateValue={intermediateScalingY}
onChange={(value) => {
@@ -179,7 +177,6 @@ export const ScalePanelContent = () => {
<CssValueInput
styleSource="local"
property={property}
keywords={[]}
value={scaleZ}
intermediateValue={intermediateScalingZ}
onChange={(value) => {
@@ -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)
}
@@ -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={() => {}}
/>
@@ -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) {
@@ -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<KeywordValue>;
options?: Array<KeywordValue | VarValue>;
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;
@@ -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);
@@ -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" },
@@ -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<KeywordValue>;
options?: Array<KeywordValue | VarValue>;
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<CssValueInputValue>({
// 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)}
</ComboboxListboxItem>
))}
</ComboboxScrollArea>
@@ -213,7 +213,6 @@ export const FilterSectionContent = ({
unit: "px",
}
}
keywords={[]}
setValue={handleFilterFunctionValueChange}
deleteProperty={() => {}}
/>
@@ -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<string, VarValue>();
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,
@@ -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,
}))}