mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
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:
1 parent
5d206b2e93
commit
54b6fd059c
7 files changed
+166
-251
No files matched your search
+72
-52
@@ -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>
|
||||
);
|
||||
};
|
||||
+59
-52
@@ -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") {
|
||||
|
||||
Reference in new issue
Block a user