Skip to content

Commit 62b0418

Browse files
authored
experimental: Use css editor in keyframes (#4952)
## Description 1. What is this PR about (link the issue and add a short description) ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent dd08852 commit 62b0418

File tree

14 files changed

+368
-353
lines changed

14 files changed

+368
-353
lines changed

apps/builder/app/builder/features/navigator/css-preview.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { StyleMap } from "@webstudio-is/css-engine";
1111
import { CollapsibleSection } from "~/builder/shared/collapsible-section";
1212
import { highlightCss } from "~/builder/shared/code-highlight";
1313
import type { ComputedStyleDecl } from "~/shared/style-object-model";
14-
import { $definedComputedStyles } from "~/builder/features/style-panel/shared/model";
14+
import { $computedStyleDeclarations } from "~/builder/features/style-panel/shared/model";
1515
import { $selectedInstance } from "~/shared/awareness";
1616

1717
const preStyle = css(textVariants.mono, {
@@ -71,12 +71,12 @@ const getCssText = (
7171
};
7272

7373
const $highlightedCss = computed(
74-
[$selectedInstance, $definedComputedStyles],
75-
(instance, definedComputedStyles) => {
74+
[$selectedInstance, $computedStyleDeclarations],
75+
(instance, computedStyleDeclarations) => {
7676
if (instance === undefined) {
7777
return;
7878
}
79-
const cssText = getCssText(definedComputedStyles, instance.id);
79+
const cssText = getCssText(computedStyleDeclarations, instance.id);
8080
return highlightCss(cssText);
8181
}
8282
);

apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
import { parseCss } from "@webstudio-is/css-data";
2-
import { StyleValue, toValue } from "@webstudio-is/css-engine";
31
import {
4-
Text,
2+
StyleValue,
3+
toValue,
4+
type CssProperty,
5+
} from "@webstudio-is/css-engine";
6+
import {
57
Grid,
68
IconButton,
79
Label,
810
Separator,
911
Tooltip,
1012
ScrollArea,
1113
theme,
14+
SectionTitle,
15+
SectionTitleButton,
16+
SectionTitleLabel,
1217
Box,
18+
Button,
1319
} from "@webstudio-is/design-system";
1420
import { MinusIcon, PlusIcon } from "@webstudio-is/icons";
1521
import type { AnimationKeyframe } from "@webstudio-is/sdk";
@@ -18,10 +24,10 @@ import {
1824
CssValueInput,
1925
type IntermediateStyleValue,
2026
} from "~/builder/features/style-panel/shared/css-value-input";
21-
import { toKebabCase } from "~/builder/features/style-panel/shared/keyword-utils";
22-
import { CodeEditor } from "~/builder/shared/code-editor";
2327
import { useIds } from "~/shared/form-utils";
2428
import { calcOffsets, findInsertionIndex, moveItem } from "./keyframe-helpers";
29+
import { CssEditor, type CssEditorApi } from "~/builder/shared/css-editor";
30+
import type { ComputedStyleDecl } from "~/shared/style-object-model";
2531

2632
const unitOptions = [
2733
{
@@ -125,21 +131,32 @@ const Keyframe = ({
125131
onChange: (value: AnimationKeyframe | undefined) => void;
126132
}) => {
127133
const ids = useIds(["offset"]);
134+
const apiRef = useRef<CssEditorApi>();
128135

129-
const cssProperties = useMemo(() => {
130-
let result = ``;
131-
for (const [property, style] of Object.entries(value.styles)) {
132-
result = `${result}${toKebabCase(property)}: ${toValue(style)};\n`;
133-
}
134-
return result;
135-
}, [value.styles]);
136+
const declarations: Array<ComputedStyleDecl> = useMemo(
137+
() =>
138+
(Object.keys(value.styles) as Array<CssProperty>).map((property) => {
139+
const styleValue = value.styles[property];
140+
return {
141+
property,
142+
source: { name: "local" },
143+
cascadedValue: styleValue,
144+
computedValue: styleValue,
145+
usedValue: styleValue,
146+
};
147+
}),
148+
[value.styles]
149+
);
136150

137151
return (
138152
<>
139153
<Grid
140154
gap={1}
141155
align={"center"}
142-
css={{ gridTemplateColumns: "1fr 1fr auto" }}
156+
css={{
157+
gridTemplateColumns: "1fr 1fr auto",
158+
paddingInline: theme.panel.paddingInline,
159+
}}
143160
>
144161
<Label htmlFor={ids.offset}>Offset</Label>
145162
<OffsetInput
@@ -157,25 +174,49 @@ const Keyframe = ({
157174
</Tooltip>
158175
</Grid>
159176
<Grid>
160-
<CodeEditor
161-
lang="css-properties"
162-
size="keyframe"
163-
value={cssProperties}
164-
onChange={() => {
165-
/* do nothing */
177+
<CssEditor
178+
showSearch={false}
179+
propertiesPosition="top"
180+
virtualize={false}
181+
declarations={declarations}
182+
apiRef={apiRef}
183+
onAddDeclarations={(addedStyleMap) => {
184+
const styles = { ...value.styles };
185+
for (const [property, value] of addedStyleMap) {
186+
styles[property] = value;
187+
}
188+
onChange({ ...value, styles });
189+
}}
190+
onDeleteProperty={(property, options = {}) => {
191+
if (options.isEphemeral === true) {
192+
return;
193+
}
194+
const styles = { ...value.styles };
195+
delete styles[property];
196+
onChange({ ...value, styles });
166197
}}
167-
onChangeComplete={(cssText) => {
168-
const parsedStyles = parseCss(`selector{${cssText}}`);
169-
onChange({
170-
...value,
171-
styles: parsedStyles.reduce(
172-
(r, { property, value }) => ({ ...r, [property]: value }),
173-
{}
174-
),
175-
});
198+
onSetProperty={(property) => {
199+
return (newValue) => {
200+
const styles = { ...value.styles, [property]: newValue };
201+
onChange({ ...value, styles });
202+
};
203+
}}
204+
onDeleteAllDeclarations={() => {
205+
onChange({ ...value, styles: {} });
176206
}}
177207
/>
178208
</Grid>
209+
<Box css={{ paddingInline: theme.panel.paddingInline }}>
210+
<Button
211+
onClick={() => {
212+
apiRef.current?.showAddStyleInput();
213+
}}
214+
prefix={<PlusIcon />}
215+
color="ghost"
216+
>
217+
Add
218+
</Button>
219+
</Box>
179220
</>
180221
);
181222
};
@@ -187,8 +228,6 @@ export const Keyframes = ({
187228
value: AnimationKeyframe[];
188229
onChange: (value: AnimationKeyframe[]) => void;
189230
}) => {
190-
const ids = useIds(["addKeyframe"]);
191-
192231
// To preserve focus on children swap
193232
const keyRefs = useRef(
194233
Array.from({ length: keyframes.length }, (_, index) => index)
@@ -204,43 +243,24 @@ export const Keyframes = ({
204243
const offsets = calcOffsets(keyframes);
205244

206245
return (
207-
<Grid
208-
css={{
209-
minHeight: 0,
210-
}}
211-
gap={1}
212-
>
213-
<Grid
214-
gap={1}
215-
align={"center"}
216-
css={{
217-
paddingInline: theme.panel.paddingInline,
218-
gridTemplateColumns: "1fr auto",
219-
}}
220-
>
221-
<Label htmlFor={ids.addKeyframe}>
222-
<Text variant={"titles"}>Keyframes</Text>
223-
</Label>
224-
<IconButton
225-
id={ids.addKeyframe}
226-
onClick={() => {
227-
onChange([...keyframes, { offset: undefined, styles: {} }]);
228-
keyRefs.current = [...keyRefs.current, keyframes.length];
229-
}}
230-
>
231-
<PlusIcon />
232-
</IconButton>
233-
</Grid>
234-
<Box
235-
css={{
236-
paddingInline: theme.panel.paddingInline,
237-
}}
246+
<Grid gap={1}>
247+
<SectionTitle
248+
collapsible={false}
249+
suffix={
250+
<SectionTitleButton
251+
tabIndex={0}
252+
prefix={<PlusIcon />}
253+
onClick={() => {
254+
onChange([...keyframes, { offset: undefined, styles: {} }]);
255+
keyRefs.current = [...keyRefs.current, keyframes.length];
256+
}}
257+
/>
258+
}
238259
>
239-
<Separator />
240-
</Box>
241-
260+
<SectionTitleLabel>Keyframes</SectionTitleLabel>
261+
</SectionTitle>
242262
<ScrollArea>
243-
<Grid gap={2} css={{ padding: theme.panel.padding }}>
263+
<Grid gap={2}>
244264
{keyframes.map((value, index) => (
245265
<Fragment key={keyRefs.current[index]}>
246266
{index > 0 && <Separator />}

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import { useComputedStyles } from "../../shared/model";
2121
import { getDots } from "../../shared/style-section";
2222
import { CssEditor, type CssEditorApi } from "../../../../shared/css-editor";
23-
import { $advancedStylesLonghands } from "./stores";
23+
import { $advancedStyleDeclarations } from "./stores";
2424
import { $selectedInstanceKey } from "~/shared/awareness";
2525
import { getSetting } from "~/builder/shared/client-settings";
2626

@@ -38,7 +38,6 @@ const AdvancedStyleSection = (props: {
3838
const styles = useComputedStyles(properties);
3939
return (
4040
<CollapsibleSectionRoot
41-
label={label}
4241
isOpen={isOpen}
4342
onOpenChange={setIsOpen}
4443
fullWidth
@@ -65,9 +64,11 @@ const AdvancedStyleSection = (props: {
6564
};
6665

6766
export const Section = () => {
68-
const styleMap = useStore($advancedStylesLonghands);
67+
const advancedStyleDeclarations = useStore($advancedStyleDeclarations);
6968
const apiRef = useRef<CssEditorApi>();
70-
const properties = Array.from(styleMap.keys());
69+
const properties = advancedStyleDeclarations.map(
70+
(styleDecl) => styleDecl.property
71+
);
7172
const selectedInstanceKey = useStore($selectedInstanceKey);
7273
// Memorizing recent properties by instance id, so that when user switches between instances and comes back
7374
// they are still in-place
@@ -135,7 +136,7 @@ export const Section = () => {
135136
}}
136137
>
137138
<CssEditor
138-
styleMap={styleMap}
139+
declarations={advancedStyleDeclarations}
139140
onDeleteProperty={handleDeleteProperty}
140141
onSetProperty={setProperty}
141142
onAddDeclarations={handleAddDeclarations}

apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { computed } from "nanostores";
2-
import type { CssProperty, CssStyleMap } from "@webstudio-is/css-engine";
3-
import { $matchingBreakpoints, getDefinedStyles } from "../../shared/model";
2+
import type { CssProperty } from "@webstudio-is/css-engine";
3+
import { $computedStyleDeclarations } from "../../shared/model";
44
import { sections } from "../sections";
5-
import {
6-
$registeredComponentMetas,
7-
$styles,
8-
$styleSourceSelections,
9-
} from "~/shared/nano-states";
10-
import { $selectedInstancePath } from "~/shared/awareness";
115
import { $settings } from "~/builder/shared/client-settings";
6+
import type { ComputedStyleDecl } from "~/shared/style-object-model";
7+
import { ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
128

139
// @todo will be fully deleted https://github.com/webstudio-is/webstudio/issues/4871
1410
const initialProperties = new Set<CssProperty>([
@@ -19,64 +15,47 @@ const initialProperties = new Set<CssProperty>([
1915
"user-select",
2016
]);
2117

22-
export const $advancedStylesLonghands = computed(
23-
[
24-
// prevent showing properties inherited from root
25-
// to not bloat advanced panel
26-
$selectedInstancePath,
27-
$registeredComponentMetas,
28-
$styleSourceSelections,
29-
$matchingBreakpoints,
30-
$styles,
31-
$settings,
32-
],
33-
(
34-
instancePath,
35-
metas,
36-
styleSourceSelections,
37-
matchingBreakpoints,
38-
styles,
39-
settings
40-
) => {
41-
const advancedStyles: CssStyleMap = new Map();
42-
43-
if (instancePath === undefined) {
44-
return advancedStyles;
45-
}
46-
47-
const definedStyles = getDefinedStyles({
48-
instancePath,
49-
metas,
50-
matchingBreakpoints,
51-
styleSourceSelections,
52-
styles,
53-
});
54-
18+
export const $advancedStyleDeclarations = computed(
19+
[$computedStyleDeclarations, $settings],
20+
(computedStyleDeclarations, settings) => {
21+
const advancedStyles: Array<ComputedStyleDecl> = [];
5522
// All properties used by the panels except the advanced panel
5623
const visualProperties = new Set<CssProperty>([]);
5724
for (const { properties } of sections.values()) {
5825
for (const property of properties) {
5926
visualProperties.add(property);
6027
}
6128
}
62-
for (const style of definedStyles) {
63-
const { property, value, listed } = style;
64-
const hyphenatedProperty = property;
29+
for (const styleDecl of computedStyleDeclarations) {
30+
// We don't want to show the massive amount of root variables on child instances.
31+
// @todo add filters to the UI to allow user decide.
32+
if (
33+
styleDecl.source.name === "remote" &&
34+
styleDecl.source.instanceId === ROOT_INSTANCE_ID
35+
) {
36+
continue;
37+
}
38+
const { property, listed } = styleDecl;
6539
// When property is listed, it was added from advanced panel.
6640
// If we are in advanced mode, we show them all.
6741
if (
68-
visualProperties.has(hyphenatedProperty) === false ||
42+
visualProperties.has(property) === false ||
6943
listed ||
7044
settings.stylePanelMode === "advanced"
7145
) {
72-
advancedStyles.set(hyphenatedProperty, value);
46+
advancedStyles.push(styleDecl);
7347
}
7448
}
7549
// In advanced mode we assume user knows the properties they need, so we don't need to show these.
7650
// @todo https://github.com/webstudio-is/webstudio/issues/4871
7751
if (settings.stylePanelMode !== "advanced") {
78-
for (const initialProperty of initialProperties) {
79-
advancedStyles.set(initialProperty, { type: "guaranteedInvalid" });
52+
for (const property of initialProperties) {
53+
const styleDecl = computedStyleDeclarations.find(
54+
(styleDecl) => styleDecl.property === property
55+
);
56+
if (styleDecl) {
57+
advancedStyles.push(styleDecl);
58+
}
8059
}
8160
}
8261

0 commit comments

Comments
 (0)