experimental: migrate shadows sections to style object model (#4092)

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

Refactoed box shadow and text shadow sections with "repeated style"
control and switched to new style engine. Simplified layer label logic.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2024-09-09 14:26:06 +02:00
1 parent 5d206b2e93
commit 54b6fd059c
7 files changed
+166 -251

No files matched your search

@@ -1,12 +1,18 @@
import { Tooltip, Text } from "@webstudio-is/design-system";
import { InfoCircleIcon } from "@webstudio-is/icons";
import type { LayersValue, StyleProperty } from "@webstudio-is/css-engine";
import { colord, type RgbaColor } from "colord";
import {
toValue,
type StyleProperty,
type StyleValue,
} from "@webstudio-is/css-engine";
import { RepeatedStyleSection } from "../../shared/style-section";
import type { SectionProps } from "../shared/section";
import { LayersList } from "../../style-layers-list";
import { addLayer } from "../../style-layer-utils";
import { parseCssValue } from "@webstudio-is/css-data";
import { ShadowContent } from "../../shared/shadow-content";
import { useComputedStyleDecl } from "../../shared/model";
import {
addRepeatedStyleItem,
editRepeatedStyleItem,
RepeatedStyle,
} from "../../shared/repeated-style";
import { parseCssFragment } from "../../shared/parse-css-fragment";
export const properties = ["boxShadow"] satisfies [
StyleProperty,
@@ -17,9 +23,41 @@ const property: StyleProperty = properties[0];
const label = "Box Shadows";
const initialBoxShadow = "0px 2px 5px 0px rgba(0, 0, 0, 0.2)";
export const Section = (props: SectionProps) => {
const { currentStyle, deleteProperty } = props;
const value = currentStyle[property]?.value;
const getItemProps = (_index: number, layer: StyleValue) => {
const values = layer.type === "tuple" ? layer.value : [];
const labels = [];
let color: RgbaColor | undefined;
let isInset = false;
for (const item of values) {
if (item.type === "rgb") {
color = colord(toValue(item)).toRgb();
continue;
}
if (item.type === "keyword") {
if (item.value === "inset") {
isInset = true;
continue;
}
if (colord(item.value).isValid()) {
color = colord(item.value).toRgb();
continue;
}
}
labels.push(toValue(item));
}
if (isInset) {
labels.unshift("Inner Shadow:");
} else {
labels.unshift("Outer Shadow:");
}
return { label: labels.join(" "), color };
};
export const Section = () => {
const styleDecl = useComputedStyleDecl("boxShadow");
return (
<RepeatedStyleSection
@@ -27,51 +65,33 @@ export const Section = (props: SectionProps) => {
description="Adds shadow effects around an element's frame."
properties={properties}
onAdd={() => {
addLayer(
property,
parseCssValue(property, initialBoxShadow) as LayersValue,
currentStyle,
props.createBatchUpdate
addRepeatedStyleItem(
[styleDecl],
parseCssFragment(initialBoxShadow, "boxShadow")
);
}}
>
{value?.type === "layers" && value.value.length > 0 && (
<LayersList
{...props}
property={property}
value={value}
label={label}
deleteProperty={deleteProperty}
renderContent={(layerProps) => {
if (layerProps.layer.type !== "tuple") {
return <></>;
}
return (
<ShadowContent
{...layerProps}
layer={layerProps.layer}
property={property}
tooltip={
<Tooltip
variant="wrapped"
content={
<Text>
Paste a box-shadow CSS code without the property name,
for example:
<br /> <br />
<Text variant="monoBold">{initialBoxShadow}</Text>
</Text>
}
>
<InfoCircleIcon />
</Tooltip>
}
/>
);
}}
/>
)}
<RepeatedStyle
label={label}
styles={[styleDecl]}
getItemProps={getItemProps}
renderItemContent={(index, value) => (
<ShadowContent
index={index}
layer={value}
property={property}
propertyValue={toValue(value)}
onEditLayer={(index, value, options) => {
editRepeatedStyleItem(
[styleDecl],
index,
new Map([["boxShadow", value]]),
options
);
}}
/>
)}
/>
</RepeatedStyleSection>
);
};
@@ -1,12 +1,18 @@
import type { SectionProps } from "../shared/section";
import type { LayersValue, StyleProperty } from "@webstudio-is/css-engine";
import { Tooltip, Text } from "@webstudio-is/design-system";
import { InfoCircleIcon } from "@webstudio-is/icons";
import { addLayer } from "../../style-layer-utils";
import { parseCssValue } from "@webstudio-is/css-data";
import { colord, type RgbaColor } from "colord";
import {
toValue,
type StyleProperty,
type StyleValue,
} from "@webstudio-is/css-engine";
import { RepeatedStyleSection } from "../../shared/style-section";
import { LayersList } from "../../style-layers-list";
import { ShadowContent } from "../../shared/shadow-content";
import {
addRepeatedStyleItem,
editRepeatedStyleItem,
RepeatedStyle,
} from "../../shared/repeated-style";
import { parseCssFragment } from "../../shared/parse-css-fragment";
import { useComputedStyleDecl } from "../../shared/model";
export const properties = ["textShadow"] satisfies [
StyleProperty,
@@ -17,9 +23,28 @@ const property: StyleProperty = properties[0];
const label = "Text Shadows";
const initialTextShadow = "0px 2px 5px rgba(0, 0, 0, 0.2)";
export const Section = (props: SectionProps) => {
const { currentStyle, createBatchUpdate, deleteProperty } = props;
const value = currentStyle[property]?.value;
const getItemProps = (_index: number, layer: StyleValue) => {
const values = layer.type === "tuple" ? layer.value : [];
const labels = ["Text Shadow:"];
let color: RgbaColor | undefined;
for (const item of values) {
if (item.type === "rgb") {
color = colord(toValue(item)).toRgb();
continue;
}
if (item.type === "keyword" && colord(item.value).isValid()) {
color = colord(item.value).toRgb();
continue;
}
labels.push(toValue(item));
}
return { label: labels.join(" "), color };
};
export const Section = () => {
const styleDecl = useComputedStyleDecl("textShadow");
return (
<RepeatedStyleSection
@@ -27,51 +52,33 @@ export const Section = (props: SectionProps) => {
description="Adds shadow effects around a text."
properties={properties}
onAdd={() => {
addLayer(
property,
parseCssValue(property, initialTextShadow) as LayersValue,
currentStyle,
createBatchUpdate
addRepeatedStyleItem(
[styleDecl],
parseCssFragment(initialTextShadow, "textShadow")
);
}}
>
{value?.type === "layers" && value.value.length > 0 && (
<LayersList
{...props}
property={property}
value={value}
label={label}
deleteProperty={deleteProperty}
renderContent={(layerProps) => {
if (layerProps.layer.type !== "tuple") {
return <></>;
}
return (
<ShadowContent
{...layerProps}
layer={layerProps.layer}
property={property}
tooltip={
<Tooltip
variant="wrapped"
content={
<Text>
Paste a text-shadow CSS code without the property name,
for example:
<br /> <br />
<Text variant="monoBold">{initialTextShadow}</Text>
</Text>
}
>
<InfoCircleIcon />
</Tooltip>
}
/>
);
}}
/>
)}
<RepeatedStyle
label={label}
styles={[styleDecl]}
getItemProps={getItemProps}
renderItemContent={(index, value) => (
<ShadowContent
index={index}
layer={value}
property={property}
propertyValue={toValue(value)}
onEditLayer={(index, value, options) => {
editRepeatedStyleItem(
[styleDecl],
index,
new Map([["textShadow", value]]),
options
);
}}
/>
)}
/>
</RepeatedStyleSection>
);
};
@@ -228,7 +228,6 @@ export const FilterSectionContent = ({
index={index}
property="dropShadow"
layer={layer.args}
tooltip={<></>}
propertyValue={toValue(layer.args)}
onEditLayer={(_, dropShadowLayers, options) => {
handleComplete(
@@ -236,7 +235,6 @@ export const FilterSectionContent = ({
options
);
}}
deleteProperty={() => {}}
hideCodeEditor={true}
/>
) : undefined}
@@ -141,7 +141,7 @@ export const RepeatedStyle = (props: {
index: number,
primaryValue: StyleValue
) => { label: string; color?: RgbaColor };
renderItemContent: (index: number) => JSX.Element;
renderItemContent: (index: number, primaryValue: StyleValue) => JSX.Element;
}) => {
const transformLayers = createLayersTransformer(props.styles);
const { label, styles, getItemProps, renderItemContent } = props;
@@ -183,7 +183,7 @@ export const RepeatedStyle = (props: {
<FloatingPanel
key={index}
title={label}
content={renderItemContent(index)}
content={renderItemContent(index, primaryValue)}
>
<CssValueListItem
id={id}
@@ -1,4 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import {
hyphenateProperty,
toValue,
type InvalidValue,
type LayersValue,
@@ -17,6 +19,7 @@ import {
Grid,
Label,
Separator,
Text,
TextArea,
textVariants,
theme,
@@ -24,15 +27,15 @@ import {
ToggleGroupButton,
Tooltip,
} from "@webstudio-is/design-system";
import { ShadowInsetIcon, ShadowNormalIcon } from "@webstudio-is/icons";
import { useMemo, useState } from "react";
import {
InfoCircleIcon,
ShadowInsetIcon,
ShadowNormalIcon,
} from "@webstudio-is/icons";
import type { IntermediateStyleValue } from "../shared/css-value-input";
import { CssValueInputContainer } from "../shared/css-value-input";
import { toPascalCase } from "../shared/keyword-utils";
import type {
DeleteProperty,
StyleUpdateOptions,
} from "../shared/use-style-data";
import type { StyleUpdateOptions } from "../shared/use-style-data";
import { parseCssFragment } from "./parse-css-fragment";
import { PropertyInlineLabel } from "../property-label";
import { styleConfigByName } from "./configs";
@@ -65,15 +68,13 @@ import { ColorPicker } from "./color-picker";
type ShadowContentProps = {
index: number;
property: "boxShadow" | "textShadow" | "dropShadow";
layer: TupleValue;
layer: StyleValue;
propertyValue: string;
tooltip: JSX.Element;
onEditLayer: (
index: number,
layers: LayersValue,
options: StyleUpdateOptions
) => void;
deleteProperty: DeleteProperty;
hideCodeEditor?: boolean;
};
@@ -122,18 +123,20 @@ export const ShadowContent = ({
index,
property,
propertyValue,
tooltip,
hideCodeEditor = false,
onEditLayer,
deleteProperty,
}: ShadowContentProps) => {
const [intermediateValue, setIntermediateValue] = useState<
IntermediateStyleValue | InvalidValue | undefined
>();
const layerValues = useMemo<ExtractedShadowProperties>(() => {
>({ type: "intermediate", value: propertyValue });
useEffect(() => {
setIntermediateValue({ type: "intermediate", value: propertyValue });
return extractShadowProperties(layer);
}, [layer, propertyValue]);
}, [propertyValue]);
const layerValues = useMemo<ExtractedShadowProperties>(() => {
return extractShadowProperties(
layer.type === "tuple" ? layer : { type: "tuple", value: [] }
);
}, [layer]);
const { offsetX, offsetY, blur, spread, color, inset } = layerValues;
const colorControlProp = color ?? {
@@ -379,7 +382,21 @@ export const ShadowContent = ({
<Label>
<Flex align={"center"} gap={1}>
Code
{tooltip}
<Tooltip
variant="wrapped"
content={
<Text>
Paste a {hyphenateProperty(property)} CSS code without the
property name, for example:
<br /> <br />
<Text variant="monoBold">
0px 2px 5px 0px rgba(0, 0, 0, 0.2)
</Text>
</Text>
}
>
<InfoCircleIcon />
</Tooltip>
</Flex>
</Label>
<TextArea
@@ -397,15 +414,6 @@ export const ShadowContent = ({
handleComplete();
event.preventDefault();
}
// dropShadow is a function under the `filter` and `backdrop-filter` properties.
// which are managed using tupes. If we delete here. It delets the whole `filter` / `backdrop-filter` property.
// So, we avoid this ephermal operation for dropShadow.
if (event.key === "Escape" && property !== "dropShadow") {
deleteProperty(property, { isEphemeral: true });
setIntermediateValue(undefined);
event.preventDefault();
}
}}
/>
</Flex>
@@ -363,69 +363,6 @@ describe("boxShadowUtils", () => {
});
});
test("Generates humane layer names for shadow style layer", () => {
expect(
getHumanizedTextFromLayer(
"boxShadow",
(
parseCssValue(
"boxShadow",
"inset 2px 2px 5px rgba(0, 0, 0, 0.5)"
) as LayersValue
).value[0] as TupleValue
)
).toMatchInlineSnapshot(`
{
"color": {
"a": 0.5,
"b": 0,
"g": 0,
"r": 0,
},
"name": "Inner Shadow: 2px 2px 5px",
"value": "inset 2px 2px 5px rgba(0, 0, 0, 0.5)",
}
`);
expect(
getHumanizedTextFromLayer(
"boxShadow",
(
parseCssValue(
"boxShadow",
"3px 3px 10px rgba(255, 0, 0, 0.7)"
) as LayersValue
).value[0] as TupleValue
)
).toMatchInlineSnapshot(`
{
"color": {
"a": 0.7,
"b": 0,
"g": 0,
"r": 255,
},
"name": "Outer Shadow: 3px 3px 10px",
"value": "3px 3px 10px rgba(255, 0, 0, 0.7)",
}
`);
// colors is returning isValid as false in node env for 'red'
expect(
getHumanizedTextFromLayer(
"textShadow",
(parseCssValue("textShadow", "1px 1px 2px red") as LayersValue)
.value[0] as TupleValue
)
).toMatchInlineSnapshot(`
{
"color": undefined,
"name": "Text Shadow: 1px 1px 2px red",
"value": "1px 1px 2px red",
}
`);
});
test("Generates humane layer names for filter style layer", () => {
expect(
getHumanizedTextFromLayer(
@@ -12,7 +12,6 @@ import type {
CreateBatchUpdate,
StyleUpdateOptions,
} from "./shared/use-style-data";
import { colord, type RgbaColor } from "colord";
import { humanizeString } from "~/shared/string-utils";
import type { LayerListProperty } from "./style-layers-list";
@@ -192,60 +191,6 @@ export const getHumanizedTextFromLayer = (
layer: StyleValue
) => {
switch (property) {
case "textShadow":
case "boxShadow":
if (layer.type === "tuple") {
const name = [];
let color: RgbaColor | undefined;
const properties = [...layer.value];
if (property === "boxShadow") {
const insetKeyword = layer.value.find(
(item) => item.type === "keyword" && item.value === "inset"
);
if (insetKeyword !== undefined) {
name.push("Inner Shadow: ");
properties.splice(properties.indexOf(insetKeyword), 1);
} else {
name.push("Outer Shadow: ");
}
}
if (property === "textShadow") {
name.push("Text Shadow: ");
}
for (const item of properties) {
if (item.type === "unit") {
const value = toValue(item);
name.push(value);
}
if (item.type === "rgb") {
color = colord(toValue(item)).toRgb();
}
if (item.type === "keyword") {
if (colord(item.value).isValid() === false) {
name.push(item.value);
} else {
color = colord(item.value).toRgb();
}
}
if (item.type === "unparsed") {
name.push(item.value);
}
if (item.type === "function") {
const value = `${item.name}(${toValue(item.args)})`;
name.push(value);
}
}
return { name: name.join(" "), value: toValue(layer), color };
}
break;
case "filter":
case "backdropFilter":
if (layer.type === "function") {