From 060ee6d2e3aacf9fb19e6c5a22056d44a92a5635 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Wed, 16 Apr 2025 01:50:04 +0700 Subject: [PATCH] experimental: split keyframe styles into popover We want to add a few predefined fields in every keyframe. Here splitted each keyframe into separate popover to avoid overloading. --- .../animation/animation-keyframes.tsx | 256 +++++++++--------- .../animation/animation-panel-content.tsx | 34 ++- 2 files changed, 148 insertions(+), 142 deletions(-) diff --git a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx index a0596dcd9372..43f73f5cc432 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx @@ -5,24 +5,24 @@ import { } from "@webstudio-is/css-engine"; import { Grid, - IconButton, Label, - Separator, Tooltip, - ScrollArea, theme, SectionTitle, SectionTitleButton, SectionTitleLabel, + FloatingPanel, + CssValueListItem, + SmallIconButton, + CssValueListArrowFocus, } from "@webstudio-is/design-system"; import { MinusIcon, PlusIcon } from "@webstudio-is/icons"; import type { AnimationKeyframe } from "@webstudio-is/sdk"; -import { Fragment, useMemo, useRef, useState } from "react"; +import { Fragment, useId, useMemo, useRef, useState } from "react"; import { CssValueInput, type IntermediateStyleValue, } from "~/builder/features/style-panel/shared/css-value-input"; -import { useIds } from "~/shared/form-utils"; import { calcOffsets, findInsertionIndex, moveItem } from "./keyframe-helpers"; import { CssEditor } from "~/builder/shared/css-editor"; import type { ComputedStyleDecl } from "~/shared/style-object-model"; @@ -35,6 +35,8 @@ const unitOptions = [ }, ]; +const roundOffset = (value: number) => Math.round(value * 1000) / 10; + const OffsetInput = ({ id, value, @@ -54,9 +56,7 @@ const OffsetInput = ({ []} unitOptions={unitOptions} @@ -66,11 +66,7 @@ const OffsetInput = ({ property={"font-stretch"} value={ value !== undefined - ? { - type: "unit", - value: Math.round(value * 1000) / 10, - unit: "%", - } + ? { type: "unit", value: roundOffset(value), unit: "%" } : undefined } onChange={(styleValue) => { @@ -121,20 +117,21 @@ const OffsetInput = ({ const Keyframe = ({ value, + index, offsetPlaceholder, onChange, + onDelete, }: { value: AnimationKeyframe; + index: number; offsetPlaceholder: number; - onChange: ( - value: AnimationKeyframe | undefined, - isEphemeral: boolean - ) => void; + onChange: (value: AnimationKeyframe, isEphemeral: boolean) => void; + onDelete: () => void; }) => { - const ids = useIds(["offset"]); - const declarations: Array = useMemo( + const offsetId = useId(); + const declarations = useMemo( () => - (Object.keys(value.styles) as Array).map((property) => { + (Object.keys(value.styles) as CssProperty[]).map((property) => { const styleValue = value.styles[property]; return { property, @@ -142,70 +139,91 @@ const Keyframe = ({ cascadedValue: styleValue, computedValue: styleValue, usedValue: styleValue, - }; + } satisfies ComputedStyleDecl; }), [value.styles] ); return ( - <> - - - { - onChange({ ...value, offset }, false); - }} - /> - - onChange(undefined, false)}> - - - - - - { - const styles = { ...value.styles }; - for (const [property, value] of addedStyleMap) { - styles[property] = value; - } - onChange({ ...value, styles }, false); - }} - onDeleteProperty={(property, options = {}) => { - if (options.isEphemeral === true) { - return; - } - const styles = { ...value.styles }; - delete styles[property]; - onChange({ ...value, styles }, false); - }} - onSetProperty={(property) => { - return (newValue, options) => { - const styles = { ...value.styles, [property]: newValue }; - onChange({ ...value, styles }, options?.isEphemeral ?? false); - }; - }} - onDeleteAllDeclarations={() => { - onChange({ ...value, styles: {} }, false); - }} - /> - - + + + + { + onChange({ ...value, offset }, false); + }} + /> + + + { + const styles = { ...value.styles }; + for (const [property, value] of addedStyleMap) { + styles[property] = value; + } + onChange({ ...value, styles }, false); + }} + onDeleteProperty={(property, options = {}) => { + if (options.isEphemeral === true) { + return; + } + const styles = { ...value.styles }; + delete styles[property]; + onChange({ ...value, styles }, false); + }} + onSetProperty={(property) => { + return (newValue, options) => { + const styles = { ...value.styles, [property]: newValue }; + onChange({ ...value, styles }, options?.isEphemeral ?? false); + }; + }} + onDeleteAllDeclarations={() => { + onChange({ ...value, styles: {} }, false); + }} + /> + + } + > + + {value.offset + ? `${roundOffset(value.offset)}%` + : `auto (${roundOffset(offsetPlaceholder)}%)`} + + } + buttons={ + + } + onClick={onDelete} + /> + + } + > + ); }; @@ -232,7 +250,7 @@ export const Keyframes = ({ const offsets = calcOffsets(keyframes); return ( - +
Keyframes - - - {keyframes.map((value, index) => ( - - {index > 0 && } - { - if (newValue === undefined && isEphemeral) { - onChange(undefined, true); - return; - } - - if (newValue === undefined) { - const newValues = [...keyframes]; - newValues.splice(index, 1); - onChange(newValues, isEphemeral); - return; - } + + {keyframes.map((value, index) => ( + + { + let newValues = [...keyframes]; + newValues[index] = newValue; - let newValues = [...keyframes]; - newValues[index] = newValue; - - const { offset } = newValue; - if (offset === undefined) { - onChange(newValues, isEphemeral); - return; - } + const { offset } = newValue; + if (offset === undefined) { + onChange(newValues, isEphemeral); + return; + } - const insertionIndex = findInsertionIndex(newValues, index); - newValues = moveItem(newValues, index, insertionIndex); - keyRefs.current = moveItem( - keyRefs.current, - index, - insertionIndex - ); + const insertionIndex = findInsertionIndex(newValues, index); + newValues = moveItem(newValues, index, insertionIndex); + keyRefs.current = moveItem( + keyRefs.current, + index, + insertionIndex + ); - onChange(newValues, isEphemeral); - }} - /> - - ))} - - - + onChange(newValues, isEphemeral); + }} + onDelete={() => { + const newValues = [...keyframes]; + newValues.splice(index, 1); + onChange(newValues, false); + }} + /> + + ))} + +
); }; diff --git a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx index 4075251e63d7..7e30e18ab60e 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx @@ -1,11 +1,11 @@ -import { useState } from "react"; +import { useState, type ReactNode } from "react"; import { Box, EnhancedTooltip, - Flex, Grid, InputField, Label, + ScrollArea, Select, SmallToggleButton, theme, @@ -295,6 +295,16 @@ const defaultRangeEnd = { unit: "%", }; +const PanelContainer = ({ children }: { children: ReactNode }) => { + return ( + + + {children} + + + ); +}; + export const AnimationPanelContent = ({ onChange, value, @@ -345,23 +355,11 @@ export const AnimationPanelContent = ({ // Flex is used to allow the Keyframes to overflow without setting // gridTemplateRows: auto auto 1fr return ( - + - + ); };