mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
experimental: Link animation range names (#5056)
## Description <img width="523" alt="image" src="https://github.com/user-attachments/assets/81a860c5-fcd3-4f0c-af60-9073216a8da6" /> ## 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
This commit is contained in:
1 parent
1d9ae2ad29
commit
edceea2760
1 file changed
+137
-77
+137
-77
@@ -1,11 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
EnhancedTooltip,
|
||||
Flex,
|
||||
Grid,
|
||||
InputField,
|
||||
Label,
|
||||
Select,
|
||||
SmallToggleButton,
|
||||
theme,
|
||||
toast,
|
||||
} from "@webstudio-is/design-system";
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
} from "@webstudio-is/css-engine";
|
||||
import { Keyframes } from "./animation-keyframes";
|
||||
import { humanizeString } from "~/shared/string-utils";
|
||||
import { Link2Icon, Link2UnlinkedIcon } from "@webstudio-is/icons";
|
||||
|
||||
const fillModeDescriptions: Record<
|
||||
NonNullable<ViewAnimation["timing"]["fill"]>,
|
||||
@@ -215,11 +218,27 @@ type AnimationPanelContentProps = {
|
||||
((value: undefined, isEphemeral: true) => void);
|
||||
};
|
||||
|
||||
const defaultRangeStart = {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "%",
|
||||
};
|
||||
|
||||
const defaultRangeEnd = {
|
||||
type: "unit",
|
||||
value: 100,
|
||||
unit: "%",
|
||||
};
|
||||
|
||||
export const AnimationPanelContent = ({
|
||||
onChange,
|
||||
value,
|
||||
type,
|
||||
}: AnimationPanelContentProps) => {
|
||||
const [isLinked, setIsLinked] = useState(
|
||||
value.timing.rangeStart?.[0] === value.timing.rangeEnd?.[0]
|
||||
);
|
||||
|
||||
const fieldIds = useIds([
|
||||
"rangeStartName",
|
||||
"rangeStartValue",
|
||||
@@ -302,12 +321,13 @@ export const AnimationPanelContent = ({
|
||||
gap={1}
|
||||
align={"center"}
|
||||
css={{
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gridTemplateColumns: "1fr 16px 1fr",
|
||||
paddingInline: theme.panel.paddingInline,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={fieldIds.fill}>Fill Mode</Label>
|
||||
<div />
|
||||
<Label htmlFor={fieldIds.easing}>Easing</Label>
|
||||
|
||||
<Select
|
||||
@@ -358,6 +378,7 @@ export const AnimationPanelContent = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
<EasingInput
|
||||
id={fieldIds.easing}
|
||||
value={value.timing.easing}
|
||||
@@ -384,13 +405,14 @@ export const AnimationPanelContent = ({
|
||||
gap={1}
|
||||
align={"center"}
|
||||
css={{
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gridTemplateColumns: "1fr 16px 1fr",
|
||||
paddingInline: theme.panel.paddingInline,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={fieldIds.rangeStartName}>Range Start</Label>
|
||||
<Label htmlFor={fieldIds.rangeStartValue}>Value</Label>
|
||||
<div />
|
||||
<Label htmlFor={fieldIds.rangeEndName}>Range End</Label>
|
||||
|
||||
<Select
|
||||
id={fieldIds.rangeStartName}
|
||||
@@ -423,12 +445,14 @@ export const AnimationPanelContent = ({
|
||||
...value.timing,
|
||||
rangeStart: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeStart?.[1] ?? {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "%",
|
||||
},
|
||||
value.timing.rangeStart?.[1] ?? defaultRangeStart,
|
||||
],
|
||||
rangeEnd: isLinked
|
||||
? [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? defaultRangeEnd,
|
||||
]
|
||||
: value.timing.rangeEnd,
|
||||
},
|
||||
},
|
||||
true
|
||||
@@ -442,18 +466,117 @@ export const AnimationPanelContent = ({
|
||||
...value.timing,
|
||||
rangeStart: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeStart?.[1] ?? {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "%",
|
||||
},
|
||||
value.timing.rangeStart?.[1] ?? defaultRangeStart,
|
||||
],
|
||||
rangeEnd: isLinked
|
||||
? [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? defaultRangeEnd,
|
||||
]
|
||||
: value.timing.rangeEnd,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Grid>
|
||||
<EnhancedTooltip
|
||||
content={isLinked ? "Unlink range names" : "Link range names"}
|
||||
>
|
||||
<SmallToggleButton
|
||||
pressed={isLinked}
|
||||
onPressedChange={(pressed) => {
|
||||
setIsLinked(pressed);
|
||||
if (pressed) {
|
||||
handleChange(
|
||||
{
|
||||
...value,
|
||||
timing: {
|
||||
...value.timing,
|
||||
rangeEnd: pressed
|
||||
? [
|
||||
value.timing.rangeStart?.[0] ?? "entry",
|
||||
value.timing.rangeEnd?.[1] ?? defaultRangeEnd,
|
||||
]
|
||||
: value.timing.rangeEnd,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}}
|
||||
variant="normal"
|
||||
icon={isLinked ? <Link2Icon /> : <Link2UnlinkedIcon />}
|
||||
/>
|
||||
</EnhancedTooltip>
|
||||
</Grid>
|
||||
<Select
|
||||
id={fieldIds.rangeEndName}
|
||||
options={timelineRangeNames}
|
||||
getLabel={humanizeString}
|
||||
value={value.timing.rangeEnd?.[0] ?? timelineRangeNames[0]!}
|
||||
getDescription={(timelineRangeName: string) => (
|
||||
<Box
|
||||
css={{
|
||||
width: theme.spacing[28],
|
||||
}}
|
||||
>
|
||||
{
|
||||
timelineRangeDescriptions[
|
||||
timelineRangeName as keyof typeof timelineRangeDescriptions
|
||||
]
|
||||
}
|
||||
</Box>
|
||||
)}
|
||||
onItemHighlight={(timelineRangeName) => {
|
||||
if (timelineRangeName === undefined) {
|
||||
handleChange(undefined, true);
|
||||
return;
|
||||
}
|
||||
handleChange(
|
||||
{
|
||||
...value,
|
||||
timing: {
|
||||
...value.timing,
|
||||
rangeEnd: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? defaultRangeEnd,
|
||||
],
|
||||
rangeStart: isLinked
|
||||
? [
|
||||
timelineRangeName,
|
||||
value.timing.rangeStart?.[1] ?? defaultRangeStart,
|
||||
]
|
||||
: value.timing.rangeStart,
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
}}
|
||||
onChange={(timelineRangeName) => {
|
||||
handleChange(
|
||||
{
|
||||
...value,
|
||||
timing: {
|
||||
...value.timing,
|
||||
rangeEnd: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? defaultRangeEnd,
|
||||
],
|
||||
rangeStart: isLinked
|
||||
? [
|
||||
timelineRangeName,
|
||||
value.timing.rangeStart?.[1] ?? defaultRangeStart,
|
||||
]
|
||||
: value.timing.rangeStart,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<RangeValueInput
|
||||
id={fieldIds.rangeStartValue}
|
||||
value={
|
||||
@@ -486,70 +609,7 @@ export const AnimationPanelContent = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Label htmlFor={fieldIds.rangeEndName}>Range End</Label>
|
||||
<Label htmlFor={fieldIds.rangeEndValue}>Value</Label>
|
||||
<Select
|
||||
id={fieldIds.rangeEndName}
|
||||
options={timelineRangeNames}
|
||||
getLabel={humanizeString}
|
||||
value={value.timing.rangeEnd?.[0] ?? timelineRangeNames[0]!}
|
||||
getDescription={(timelineRangeName: string) => (
|
||||
<Box
|
||||
css={{
|
||||
width: theme.spacing[28],
|
||||
}}
|
||||
>
|
||||
{
|
||||
timelineRangeDescriptions[
|
||||
timelineRangeName as keyof typeof timelineRangeDescriptions
|
||||
]
|
||||
}
|
||||
</Box>
|
||||
)}
|
||||
onItemHighlight={(timelineRangeName) => {
|
||||
if (timelineRangeName === undefined) {
|
||||
handleChange(undefined, true);
|
||||
return;
|
||||
}
|
||||
handleChange(
|
||||
{
|
||||
...value,
|
||||
timing: {
|
||||
...value.timing,
|
||||
rangeEnd: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "%",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
}}
|
||||
onChange={(timelineRangeName) => {
|
||||
handleChange(
|
||||
{
|
||||
...value,
|
||||
timing: {
|
||||
...value.timing,
|
||||
rangeEnd: [
|
||||
timelineRangeName,
|
||||
value.timing.rangeEnd?.[1] ?? {
|
||||
type: "unit",
|
||||
value: 0,
|
||||
unit: "%",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
<RangeValueInput
|
||||
id={fieldIds.rangeEndValue}
|
||||
value={
|
||||
|
||||
Reference in new issue
Block a user