From 12d80025fc7d57a45ed975609aa3c4e1f519e431 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Tue, 24 Sep 2024 18:31:23 +0530 Subject: [PATCH 01/18] experimental: add gradient control for updating the color stops using ui --- .../backgrounds/gradient-control.stories.tsx | 64 +++++++++ .../sections/backgrounds/gradient-control.tsx | 135 ++++++++++++++++++ apps/builder/package.json | 1 + .../css-data/src/property-parsers/index.ts | 1 + .../src/property-parsers/linear-gradient.ts | 4 +- pnpm-lock.yaml | 35 +++++ 6 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx create mode 100644 apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx new file mode 100644 index 000000000000..337d16152d44 --- /dev/null +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -0,0 +1,64 @@ +import { + parseLinearGradient, + type ParsedGradient, +} from "@webstudio-is/css-data"; +import { GradientControl } from "./gradient-control"; +import { toValue } from "@webstudio-is/css-engine"; + +export default { + title: "Library/GradientControl", +}; + +export const GradientWithoutAngle = () => { + return ( + {}} + /> + ); +}; + +// The GradientControl is to just modify the stop values or add new ones. +// It always shows the angle as 90deg, unless the stops can't be showin in a rectangle. +// So, the gradient shouldn't modify even if the stop values are changed at the end, +export const GradientWithAngle = () => { + return ( + { + if (toValue(value.angle) !== "145deg") { + throw new Error( + `Gradient control modified the angle that is passed. \nReceived ${JSON.stringify(value.angle, null, 2)}` + ); + } + }} + /> + ); +}; + +export const GradientWithSideOrCorner = () => { + return ( + { + if (toValue(value.sideOrCorner) !== "to left top") { + throw new Error( + `Gradient control modified the side-or-corner value that is passed. \nReceived ${JSON.stringify(value.sideOrCorner, null, 2)}` + ); + } + }} + /> + ); +}; diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx new file mode 100644 index 000000000000..0404fb7eb27b --- /dev/null +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -0,0 +1,135 @@ +import { toValue, UnitValue } from "@webstudio-is/css-engine"; +import { Root, Range, Thumb, Track } from "@radix-ui/react-slider"; +import { useEffect, useState, useCallback } from "react"; +import { + reconstructLinearGradient, + type GradientStop, + type ParsedGradient, +} from "@webstudio-is/css-data"; +import { styled, theme, Flex } from "@webstudio-is/design-system"; + +type GradientControlProps = { + gradient: ParsedGradient; + onChange: (value: ParsedGradient) => void; +}; + +const defaultAngle: UnitValue = { + type: "unit", + value: 90, + unit: "deg", +}; + +export const GradientControl = (props: GradientControlProps) => { + const [stops, setStops] = useState>(props.gradient.stops); + const [selectedStop, setSelectedStop] = useState(); + const positions = stops.map((stop) => stop.position?.value) as number[]; + const background = reconstructLinearGradient({ + stops, + sideOrCorner: props.gradient.sideOrCorner, + angle: defaultAngle, + }); + + useEffect(() => { + const newStops: Array = []; + for (const stop of props.gradient.stops || []) { + if (stop.color !== undefined && stop.position?.value !== undefined) { + newStops.push({ + color: stop.color, + position: stop.position, + }); + } + } + setStops(newStops); + }, [props.gradient]); + + const handleValueChange = useCallback( + (newPositions: number[]) => { + const newStops: GradientStop[] = stops.map((stop, index) => ({ + ...stop, + position: { type: "unit", value: newPositions[index], unit: "%" }, + })); + + setStops(newStops); + props.onChange({ + angle: props.gradient.angle, + stops, + sideOrCorner: props.gradient.sideOrCorner, + }); + }, + [stops, props] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Backspace" && selectedStop !== undefined) { + const newStops = stops; + newStops.splice(selectedStop, 1); + setStops(newStops); + setSelectedStop(undefined); + } + }, + [stops, selectedStop] + ); + + return ( + + + + + + {stops.map((stop, index) => ( + { + setSelectedStop(index); + }} + style={{ + background: toValue(stop.color), + }} + /> + ))} + + + ); +}; + +const SliderRoot = styled(Root, { + position: "relative", + width: "100%", + height: theme.spacing[9], + border: `1px solid ${theme.colors.borderInfo}`, + borderRadius: theme.borderRadius[3], + touchAction: "none", + userSelect: "none", +}); + +const SliderRange = styled(Range, { + position: "absolute", + background: "transparent", + borderRadius: theme.borderRadius[3], +}); + +const SliderThumb = styled(Thumb, { + position: "absolute", + width: theme.spacing[9], + height: theme.spacing[9], + border: `1px solid ${theme.colors.borderInfo}`, + borderRadius: theme.borderRadius[3], + top: `-${theme.spacing[11]}`, + translate: "-9px", +}); + +export default GradientControl; diff --git a/apps/builder/package.json b/apps/builder/package.json index 4e819026c50b..da4c8ae20abc 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -40,6 +40,7 @@ "@nanostores/react": "^0.7.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-tooltip": "^1.1.2", + "@radix-ui/react-slider": "^1.2.0", "@react-aria/interactions": "^3.19.0", "@react-aria/utils": "^3.21.0", "@remix-run/node": "^2.11.0", diff --git a/packages/css-data/src/property-parsers/index.ts b/packages/css-data/src/property-parsers/index.ts index 29a95a095963..2ce95ca668fd 100644 --- a/packages/css-data/src/property-parsers/index.ts +++ b/packages/css-data/src/property-parsers/index.ts @@ -1,2 +1,3 @@ export * from "./transition"; export * from "./shadow-properties-extractor"; +export * from "./linear-gradient"; diff --git a/packages/css-data/src/property-parsers/linear-gradient.ts b/packages/css-data/src/property-parsers/linear-gradient.ts index df51f5bdb049..b9421e3b4bbc 100644 --- a/packages/css-data/src/property-parsers/linear-gradient.ts +++ b/packages/css-data/src/property-parsers/linear-gradient.ts @@ -12,13 +12,13 @@ import namesPlugin from "colord/plugins/names"; extend([namesPlugin]); -interface GradientStop { +export interface GradientStop { color?: RgbValue; position?: UnitValue; hint?: UnitValue; } -interface ParsedGradient { +export interface ParsedGradient { angle?: UnitValue; sideOrCorner?: KeywordValue; stops: GradientStop[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0ac4eafe98e..9067cebb61bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: '@radix-ui/react-select': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-slider': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-tooltip': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) @@ -4618,6 +4621,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slider@1.2.0': + resolution: {integrity: sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA==} + peerDependencies: + '@types/react': ^18.2.70 + '@types/react-dom': ^18.2.25 + react: 18.3.0-canary-14898b6a9-20240318 + react-dom: 18.3.0-canary-14898b6a9-20240318 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -12688,6 +12704,25 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 + '@radix-ui/react-slider@1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + react: 18.3.0-canary-14898b6a9-20240318 + react-dom: 18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318) + optionalDependencies: + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + '@radix-ui/react-slot@1.0.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318)': dependencies: '@babel/runtime': 7.25.0 From e9a0980b81b0be4014194228ae3bff3dbe87db3c Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Wed, 25 Sep 2024 10:32:30 +0530 Subject: [PATCH 02/18] update stories to display the result --- .../backgrounds/gradient-control.stories.tsx | 83 ++++++++++--------- .../sections/backgrounds/gradient-control.tsx | 40 +++++---- .../src/property-parsers/linear-gradient.ts | 2 +- 3 files changed, 67 insertions(+), 58 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx index 337d16152d44..07a95bea6605 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -1,64 +1,65 @@ import { parseLinearGradient, + reconstructLinearGradient, type ParsedGradient, } from "@webstudio-is/css-data"; import { GradientControl } from "./gradient-control"; -import { toValue } from "@webstudio-is/css-engine"; +import { Flex, Text } from "@webstudio-is/design-system"; +import { useState } from "react"; export default { title: "Library/GradientControl", }; export const GradientWithoutAngle = () => { + const gradientString = "linear-gradient(#e66465 0%, #9198e5 100%)"; + const [gradient, setGradient] = useState(gradientString); + return ( - {}} - /> + + { + setGradient(reconstructLinearGradient(value)); + }} + /> + {gradient} + ); }; -// The GradientControl is to just modify the stop values or add new ones. -// It always shows the angle as 90deg, unless the stops can't be showin in a rectangle. -// So, the gradient shouldn't modify even if the stop values are changed at the end, -export const GradientWithAngle = () => { +export const GradientWithAngleAndHints = () => { + const gradientString = + "linear-gradient(145deg, #ff00fa 0%, #00f497 34% 34%, #ffa800 56% 56%, #00eaff 100%)"; + const [gradient, setGradient] = useState(gradientString); + return ( - { - if (toValue(value.angle) !== "145deg") { - throw new Error( - `Gradient control modified the angle that is passed. \nReceived ${JSON.stringify(value.angle, null, 2)}` - ); - } - }} - /> + + { + setGradient(reconstructLinearGradient(value)); + }} + /> + {gradient} + ); }; export const GradientWithSideOrCorner = () => { + const gradientString = "linear-gradient(to left top, blue 0%, red 100%)"; + + const [gradient, setGradient] = useState(gradientString); + return ( - { - if (toValue(value.sideOrCorner) !== "to left top") { - throw new Error( - `Gradient control modified the side-or-corner value that is passed. \nReceived ${JSON.stringify(value.sideOrCorner, null, 2)}` - ); - } - }} - /> + + { + setGradient(reconstructLinearGradient(value)); + }} + /> + {gradient} + ); }; diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 0404fb7eb27b..03f7f802e0c3 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,12 +1,13 @@ import { toValue, UnitValue } from "@webstudio-is/css-engine"; import { Root, Range, Thumb, Track } from "@radix-ui/react-slider"; -import { useEffect, useState, useCallback } from "react"; +import { useState, useCallback } from "react"; import { reconstructLinearGradient, type GradientStop, type ParsedGradient, } from "@webstudio-is/css-data"; import { styled, theme, Flex } from "@webstudio-is/design-system"; +import { ChevronBigUpIcon } from "@webstudio-is/icons"; type GradientControlProps = { gradient: ParsedGradient; @@ -22,26 +23,16 @@ const defaultAngle: UnitValue = { export const GradientControl = (props: GradientControlProps) => { const [stops, setStops] = useState>(props.gradient.stops); const [selectedStop, setSelectedStop] = useState(); - const positions = stops.map((stop) => stop.position?.value) as number[]; + const positions = stops.map((stop) => stop.position?.value); + const hints = props.gradient.stops + .map((stop) => stop.hint?.value) + .filter(Boolean); const background = reconstructLinearGradient({ stops, sideOrCorner: props.gradient.sideOrCorner, angle: defaultAngle, }); - useEffect(() => { - const newStops: Array = []; - for (const stop of props.gradient.stops || []) { - if (stop.color !== undefined && stop.position?.value !== undefined) { - newStops.push({ - color: stop.color, - position: stop.position, - }); - } - } - setStops(newStops); - }, [props.gradient]); - const handleValueChange = useCallback( (newPositions: number[]) => { const newStops: GradientStop[] = stops.map((stop, index) => ({ @@ -52,7 +43,7 @@ export const GradientControl = (props: GradientControlProps) => { setStops(newStops); props.onChange({ angle: props.gradient.angle, - stops, + stops: newStops, sideOrCorner: props.gradient.sideOrCorner, }); }, @@ -101,6 +92,23 @@ export const GradientControl = (props: GradientControlProps) => { }} /> ))} + + {hints.map((hint) => { + return ( + + + + ); + })} ); diff --git a/packages/css-data/src/property-parsers/linear-gradient.ts b/packages/css-data/src/property-parsers/linear-gradient.ts index b9421e3b4bbc..2ad8491447e5 100644 --- a/packages/css-data/src/property-parsers/linear-gradient.ts +++ b/packages/css-data/src/property-parsers/linear-gradient.ts @@ -182,7 +182,7 @@ const getColor = ( }; export const reconstructLinearGradient = (parsed: ParsedGradient): string => { - const direction = parsed.angle || parsed.sideOrCorner; + const direction = parsed?.angle || parsed?.sideOrCorner; const stops = parsed.stops .map((stop: GradientStop) => { let result = toValue(stop.color); From d5315feb488151906dc6d1e0a224443711d373e2 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Wed, 25 Sep 2024 11:25:01 +0530 Subject: [PATCH 03/18] add comments for color-stop and hint behaviours --- .../sections/backgrounds/gradient-control.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 03f7f802e0c3..bd0bb76c13d0 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -23,16 +23,30 @@ const defaultAngle: UnitValue = { export const GradientControl = (props: GradientControlProps) => { const [stops, setStops] = useState>(props.gradient.stops); const [selectedStop, setSelectedStop] = useState(); - const positions = stops.map((stop) => stop.position?.value); + const positions = stops + .map((stop) => stop.position?.value) + .filter((item) => item !== undefined); const hints = props.gradient.stops .map((stop) => stop.hint?.value) - .filter(Boolean); + .filter((item) => item !== undefined); const background = reconstructLinearGradient({ stops, sideOrCorner: props.gradient.sideOrCorner, angle: defaultAngle, }); + // Every color stop should have a asociated position for us in-order to display the slider thumb. + // But when users manually enter linear-gradient from the advanced-panle. They might add something like this + // linear-gradient(to right, red, blue), or linear-gradient(150deg, red, blue 50%, yellow 50px) + // Browsers handles all these cases by following the rules of the css spec. + // https://www.w3.org/TR/css-images-4/#color-stop-fixup + // In order to handle such examples from the advanced tab too. We need to implement the color-stop-fix-up spec during parsing. + // But for now, we are just checking if every stop has a position or not. Since the main use-case if to add gradients from ui. + // We will never run into this case of a color-stop missing a position associated with it. + const isEveryStopHasAPosition = stops.every( + (stop) => stop.position !== undefined && stop.color !== undefined + ); + const handleValueChange = useCallback( (newPositions: number[]) => { const newStops: GradientStop[] = stops.map((stop, index) => ({ @@ -62,6 +76,10 @@ export const GradientControl = (props: GradientControlProps) => { [stops, selectedStop] ); + if (isEveryStopHasAPosition === false) { + return; + } + return ( { /> ))} + {/* + Hints are displayed as a chevron icon below the slider thumb. + Usually hints are used to display the behaviour of the color-stop that is preciding. + But, if we just move them along the UI. We will be basically altering the gradient itself. + Because the position of the hint is the position of the color-stop. And moving it along, might associate the hint + with a different color-stop. So, we are not allowing the user to move the hint along the slider. + + None of the tools are even displaying the hints at the moment. We are just displaying them so users can know + they are hints associated too. + */} {hints.map((hint) => { return ( Date: Wed, 25 Sep 2024 22:39:34 +0530 Subject: [PATCH 04/18] add interpolated color-stops when clicked in between the color stops --- .../sections/backgrounds/gradient-control.tsx | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index bd0bb76c13d0..9a9a0be39e52 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,4 +1,4 @@ -import { toValue, UnitValue } from "@webstudio-is/css-engine"; +import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; import { Root, Range, Thumb, Track } from "@radix-ui/react-slider"; import { useState, useCallback } from "react"; import { @@ -8,6 +8,10 @@ import { } from "@webstudio-is/css-data"; import { styled, theme, Flex } from "@webstudio-is/design-system"; import { ChevronBigUpIcon } from "@webstudio-is/icons"; +import { colord, extend } from "colord"; +import mixPlugin from "colord/plugins/mix"; + +extend([mixPlugin]); type GradientControlProps = { gradient: ParsedGradient; @@ -76,6 +80,62 @@ export const GradientControl = (props: GradientControlProps) => { [stops, selectedStop] ); + const handlePointerDown = useCallback( + (event: React.MouseEvent) => { + if (event.target === undefined || event.target === null) { + return; + } + + // radix-slider automatically brings the closest thumb to the clicked position. + // But, we want it be prevented. So, we can add a new color-stop where the user is cliked. + // And handle the even for scrubing when the user is dragging the thumb. + const sliderWidth = event.currentTarget.offsetWidth; + const clickedPosition = + event.clientX - event.currentTarget.getBoundingClientRect().left; + const newPosition = Math.ceil((clickedPosition / sliderWidth) * 100); + const isExistingPosition = positions.some( + (position) => Math.abs(newPosition - position) <= 8 + ); + + if (isExistingPosition === true) { + return; + } + + event.preventDefault(); + const newStopIndex = positions.findIndex( + (position) => position > newPosition + ); + + const index = newStopIndex === -1 ? stops.length : newStopIndex; + const prevColor = stops[index === 0 ? 0 : index - 1].color; + const nextColor = + stops[index === positions.length ? index - 1 : index].color; + + const interpolationColor = colord(toValue(prevColor)) + .mix(colord(toValue(nextColor)), newPosition / 100) + .toRgb(); + + const newColorStop: RgbValue = { + type: "rgb", + alpha: interpolationColor.a, + r: interpolationColor.r, + g: interpolationColor.g, + b: interpolationColor.b, + }; + + const newStops: GradientStop[] = [ + ...stops.slice(0, index), + { + color: newColorStop, + position: { type: "unit", value: newPosition, unit: "%" }, + }, + ...stops.slice(index), + ]; + setStops(newStops); + }, + [stops, positions] + ); + if (isEveryStopHasAPosition === false) { return; } @@ -95,6 +155,7 @@ export const GradientControl = (props: GradientControlProps) => { value={positions} onValueChange={handleValueChange} onKeyDown={handleKeyDown} + onPointerDown={handlePointerDown} > @@ -119,7 +180,7 @@ export const GradientControl = (props: GradientControlProps) => { with a different color-stop. So, we are not allowing the user to move the hint along the slider. None of the tools are even displaying the hints at the moment. We are just displaying them so users can know - they are hints associated too. + they are hints associated with stops if they managed to add gradient from the advanced tab. */} {hints.map((hint) => { return ( From 59d61cdfdd2de9c68ee8f4d44b5e17c44073b61f Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 27 Sep 2024 14:20:34 +0530 Subject: [PATCH 05/18] pass props when a thumb is selected on the radix slider --- .../backgrounds/gradient-control.stories.tsx | 3 + .../sections/backgrounds/gradient-control.tsx | 69 +++++++++++++++---- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx index 07a95bea6605..b84427104399 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -22,6 +22,7 @@ export const GradientWithoutAngle = () => { onChange={(value) => { setGradient(reconstructLinearGradient(value)); }} + onThumbSelected={() => {}} /> {gradient} @@ -40,6 +41,7 @@ export const GradientWithAngleAndHints = () => { onChange={(value) => { setGradient(reconstructLinearGradient(value)); }} + onThumbSelected={() => {}} /> {gradient} @@ -58,6 +60,7 @@ export const GradientWithSideOrCorner = () => { onChange={(value) => { setGradient(reconstructLinearGradient(value)); }} + onThumbSelected={() => {}} /> {gradient} diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 9a9a0be39e52..3115d5821049 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -16,6 +16,7 @@ extend([mixPlugin]); type GradientControlProps = { gradient: ParsedGradient; onChange: (value: ParsedGradient) => void; + onThumbSelected: (index: number, stop: GradientStop) => void; }; const defaultAngle: UnitValue = { @@ -27,6 +28,7 @@ const defaultAngle: UnitValue = { export const GradientControl = (props: GradientControlProps) => { const [stops, setStops] = useState>(props.gradient.stops); const [selectedStop, setSelectedStop] = useState(); + const [isHoveredOnStop, setIsHoveredOnStop] = useState(false); const positions = stops .map((stop) => stop.position?.value) .filter((item) => item !== undefined); @@ -80,24 +82,36 @@ export const GradientControl = (props: GradientControlProps) => { [stops, selectedStop] ); - const handlePointerDown = useCallback( - (event: React.MouseEvent) => { - if (event.target === undefined || event.target === null) { - return; - } - - // radix-slider automatically brings the closest thumb to the clicked position. - // But, we want it be prevented. So, we can add a new color-stop where the user is cliked. - // And handle the even for scrubing when the user is dragging the thumb. + const isStopExistsAtPosition = useCallback( + ( + event: React.MouseEvent + ): { isStopExistingAtPosition: boolean; newPosition: number } => { const sliderWidth = event.currentTarget.offsetWidth; const clickedPosition = event.clientX - event.currentTarget.getBoundingClientRect().left; const newPosition = Math.ceil((clickedPosition / sliderWidth) * 100); - const isExistingPosition = positions.some( + // The 8px buffer here is the width of the thumb. We don't want to add a new stop if the user clicks on the thumb. + const isStopExistingAtPosition = positions.some( (position) => Math.abs(newPosition - position) <= 8 ); - if (isExistingPosition === true) { + return { isStopExistingAtPosition, newPosition }; + }, + [positions] + ); + + const handlePointerDown = useCallback( + (event: React.MouseEvent) => { + if (event.target === undefined || event.target === null) { + return; + } + + // radix-slider automatically brings the closest thumb to the clicked position. + // But, we want it be prevented. For adding a new color-stop where the user clicked. + // And handle the change in values only even for scrubing when the user is dragging the thumb. + const { isStopExistingAtPosition, newPosition } = + isStopExistsAtPosition(event); + if (isStopExistingAtPosition === true) { return; } @@ -131,11 +145,23 @@ export const GradientControl = (props: GradientControlProps) => { }, ...stops.slice(index), ]; + setStops(newStops); + setIsHoveredOnStop(true); + props.onChange({ + angle: props.gradient.angle, + stops: newStops, + sideOrCorner: props.gradient.sideOrCorner, + }); }, - [stops, positions] + [stops, positions, isStopExistsAtPosition, props] ); + const handleMouseEnter = (event: React.MouseEvent) => { + const { isStopExistingAtPosition } = isStopExistsAtPosition(event); + setIsHoveredOnStop(isStopExistingAtPosition); + }; + if (isEveryStopHasAPosition === false) { return; } @@ -156,15 +182,22 @@ export const GradientControl = (props: GradientControlProps) => { onValueChange={handleValueChange} onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} + isHoveredOnStop={isHoveredOnStop} + onMouseEnter={handleMouseEnter} + onMouseMove={handleMouseEnter} + onMouseLeave={() => { + setIsHoveredOnStop(false); + }} > - + {stops.map((stop, index) => ( { setSelectedStop(index); + props.onThumbSelected(index, stop); }} style={{ background: toValue(stop.color), @@ -211,6 +244,16 @@ const SliderRoot = styled(Root, { borderRadius: theme.borderRadius[3], touchAction: "none", userSelect: "none", + variants: { + isHoveredOnStop: { + true: { + cursor: "default", + }, + false: { + cursor: "copy", + }, + }, + }, }); const SliderRange = styled(Range, { From aaebe2f07cd704bb69878e38658e1c9babb2debb Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 27 Sep 2024 23:09:52 +0530 Subject: [PATCH 06/18] allow users to change the color of a color-stop --- .../sections/backgrounds/gradient-control.tsx | 151 +++++++++++++++--- .../style-panel/shared/color-picker.tsx | 2 + 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 3115d5821049..787760ef1c9c 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,15 +1,24 @@ import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; import { Root, Range, Thumb, Track } from "@radix-ui/react-slider"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useMemo } from "react"; import { reconstructLinearGradient, type GradientStop, type ParsedGradient, } from "@webstudio-is/css-data"; -import { styled, theme, Flex } from "@webstudio-is/design-system"; +import { + styled, + theme, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + Box, +} from "@webstudio-is/design-system"; import { ChevronBigUpIcon } from "@webstudio-is/icons"; import { colord, extend } from "colord"; import mixPlugin from "colord/plugins/mix"; +import { RgbaColorPicker, type RgbColor } from "react-colorful"; extend([mixPlugin]); @@ -82,7 +91,7 @@ export const GradientControl = (props: GradientControlProps) => { [stops, selectedStop] ); - const isStopExistsAtPosition = useCallback( + const checkIfStopExistsAtPosition = useCallback( ( event: React.MouseEvent ): { isStopExistingAtPosition: boolean; newPosition: number } => { @@ -110,8 +119,10 @@ export const GradientControl = (props: GradientControlProps) => { // But, we want it be prevented. For adding a new color-stop where the user clicked. // And handle the change in values only even for scrubing when the user is dragging the thumb. const { isStopExistingAtPosition, newPosition } = - isStopExistsAtPosition(event); + checkIfStopExistsAtPosition(event); + if (isStopExistingAtPosition === true) { + event.stopPropagation(); return; } @@ -154,14 +165,43 @@ export const GradientControl = (props: GradientControlProps) => { sideOrCorner: props.gradient.sideOrCorner, }); }, - [stops, positions, isStopExistsAtPosition, props] + [stops, positions, checkIfStopExistsAtPosition, props] ); const handleMouseEnter = (event: React.MouseEvent) => { - const { isStopExistingAtPosition } = isStopExistsAtPosition(event); + const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(event); setIsHoveredOnStop(isStopExistingAtPosition); }; + const handleStopSelected = useCallback( + (index: number, stop: GradientStop) => { + setSelectedStop(index); + props.onThumbSelected(index, stop); + }, + [props] + ); + + const handleStopColorChange = useCallback( + (color: RgbValue, stopIndex: number) => { + const newStops = stops.map((stop, index) => { + if (index === stopIndex) { + return { + ...stop, + color, + }; + } + return stop; + }); + setStops(newStops); + props.onChange({ + angle: props.gradient.angle, + stops: newStops, + sideOrCorner: props.gradient.sideOrCorner, + }); + }, + [stops, props] + ); + if (isEveryStopHasAPosition === false) { return; } @@ -192,18 +232,21 @@ export const GradientControl = (props: GradientControlProps) => { - {stops.map((stop, index) => ( - { - setSelectedStop(index); - props.onThumbSelected(index, stop); - }} - style={{ - background: toValue(stop.color), - }} - /> - ))} + {stops.map((stop, index) => { + if (stop.color === undefined || stop.position === undefined) { + return; + } + + return ( + + ); + })} {/* Hints are displayed as a chevron icon below the slider thumb. @@ -236,6 +279,71 @@ export const GradientControl = (props: GradientControlProps) => { ); }; +const SliderThumbComponent = (props: { + index: number; + stop: GradientStop; + onSelected: (index: number, stop: GradientStop) => void; + onColorChange: (color: RgbValue, index: number) => void; +}) => { + const { index, stop, onSelected } = props; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const value = useMemo( + () => colord(toValue(stop.color)).toRgb(), + [stop.color] + ); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + if (event.detail === 1) { + onSelected(index, stop); + } + + if (event.detail === 2) { + setIsPopoverOpen(!isPopoverOpen); + } + }, + [index, stop, onSelected, isPopoverOpen] + ); + + const handleOnColorChange = (color: RgbColor) => { + const colordInstance = colord(color).toRgb(); + props.onColorChange( + { + type: "rgb", + alpha: colordInstance.a, + r: color.r, + g: color.g, + b: color.b, + }, + index + ); + }; + + return ( + + + + + + + event.stopPropagation()} + onMouseLeave={() => setIsPopoverOpen(false)} + onClick={(event) => event.stopPropagation()} + onChange={handleOnColorChange} + /> + + + + ); +}; + const SliderRoot = styled(Root, { position: "relative", width: "100%", @@ -264,12 +372,15 @@ const SliderRange = styled(Range, { const SliderThumb = styled(Thumb, { position: "absolute", + top: `-${theme.spacing[11]}`, + translate: "-9px", +}); + +const SliderThumbTrigger = styled(Box, { width: theme.spacing[9], height: theme.spacing[9], border: `1px solid ${theme.colors.borderInfo}`, borderRadius: theme.borderRadius[3], - top: `-${theme.spacing[11]}`, - translate: "-9px", }); export default GradientControl; diff --git a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx index b5ce3a7ab03c..4582d28931ff 100644 --- a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx @@ -248,6 +248,8 @@ export const ColorPicker = ({ ); + return prefix; + return ( Date: Sat, 28 Sep 2024 12:32:46 +0530 Subject: [PATCH 07/18] refactor and add comments for position calculation --- .../backgrounds/gradient-control.stories.tsx | 2 +- .../sections/backgrounds/gradient-control.tsx | 47 +++++++------------ 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx index b84427104399..52b52fcd3a37 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -12,7 +12,7 @@ export default { }; export const GradientWithoutAngle = () => { - const gradientString = "linear-gradient(#e66465 0%, #9198e5 100%)"; + const gradientString = "linear-gradient(black 0%, white 100%)"; const [gradient, setGradient] = useState(gradientString); return ( diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 787760ef1c9c..87c93a04afe0 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -50,13 +50,13 @@ export const GradientControl = (props: GradientControlProps) => { angle: defaultAngle, }); - // Every color stop should have a asociated position for us in-order to display the slider thumb. - // But when users manually enter linear-gradient from the advanced-panle. They might add something like this + // Every color stop should have a position asociated for us in-order to display the slider thumb. + // But when users manually enter linear-gradient from the advanced-panel. They might add something like this // linear-gradient(to right, red, blue), or linear-gradient(150deg, red, blue 50%, yellow 50px) - // Browsers handles all these cases by following the rules of the css spec. + // Browsers handels all these cases by following the rules of the css spec. // https://www.w3.org/TR/css-images-4/#color-stop-fixup - // In order to handle such examples from the advanced tab too. We need to implement the color-stop-fix-up spec during parsing. - // But for now, we are just checking if every stop has a position or not. Since the main use-case if to add gradients from ui. + // In order to handle such inputs from the advanced tab too. We need to implement the color-stop-fix-up spec during parsing. + // But for now, we are just checking if every stop has a position or not. Since the main use-case is to add gradients from ui. // We will never run into this case of a color-stop missing a position associated with it. const isEveryStopHasAPosition = stops.every( (stop) => stop.position !== undefined && stop.color !== undefined @@ -99,7 +99,8 @@ export const GradientControl = (props: GradientControlProps) => { const clickedPosition = event.clientX - event.currentTarget.getBoundingClientRect().left; const newPosition = Math.ceil((clickedPosition / sliderWidth) * 100); - // The 8px buffer here is the width of the thumb. We don't want to add a new stop if the user clicks on the thumb. + // The 8px buffer here is the width of the thumb. + // We don't want to add a new stop if the user clicks on the thumb. const isStopExistingAtPosition = positions.some( (position) => Math.abs(newPosition - position) <= 8 ); @@ -122,15 +123,14 @@ export const GradientControl = (props: GradientControlProps) => { checkIfStopExistsAtPosition(event); if (isStopExistingAtPosition === true) { - event.stopPropagation(); return; } event.preventDefault(); + // Adding a new stop when user clicks on the slider. const newStopIndex = positions.findIndex( (position) => position > newPosition ); - const index = newStopIndex === -1 ? stops.length : newStopIndex; const prevColor = stops[index === 0 ? 0 : index - 1].color; const nextColor = @@ -168,11 +168,6 @@ export const GradientControl = (props: GradientControlProps) => { [stops, positions, checkIfStopExistsAtPosition, props] ); - const handleMouseEnter = (event: React.MouseEvent) => { - const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(event); - setIsHoveredOnStop(isStopExistingAtPosition); - }; - const handleStopSelected = useCallback( (index: number, stop: GradientStop) => { setSelectedStop(index); @@ -183,15 +178,8 @@ export const GradientControl = (props: GradientControlProps) => { const handleStopColorChange = useCallback( (color: RgbValue, stopIndex: number) => { - const newStops = stops.map((stop, index) => { - if (index === stopIndex) { - return { - ...stop, - color, - }; - } - return stop; - }); + const newStops = stops; + newStops[stopIndex].color = color; setStops(newStops); props.onChange({ angle: props.gradient.angle, @@ -202,6 +190,11 @@ export const GradientControl = (props: GradientControlProps) => { [stops, props] ); + const handleMouseEnter = (event: React.MouseEvent) => { + const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(event); + setIsHoveredOnStop(isStopExistingAtPosition); + }; + if (isEveryStopHasAPosition === false) { return; } @@ -225,12 +218,10 @@ export const GradientControl = (props: GradientControlProps) => { isHoveredOnStop={isHoveredOnStop} onMouseEnter={handleMouseEnter} onMouseMove={handleMouseEnter} - onMouseLeave={() => { - setIsHoveredOnStop(false); - }} + onMouseLeave={() => setIsHoveredOnStop(false)} > - - + + {stops.map((stop, index) => { if (stop.color === undefined || stop.position === undefined) { @@ -382,5 +373,3 @@ const SliderThumbTrigger = styled(Box, { border: `1px solid ${theme.colors.borderInfo}`, borderRadius: theme.borderRadius[3], }); - -export default GradientControl; From b3c6d7d855b9efd82ad3d954bd1752f836065c5f Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Thu, 17 Oct 2024 14:27:44 +0530 Subject: [PATCH 08/18] use slider instead of root from radix --- .../style-panel/sections/backgrounds/gradient-control.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 87c93a04afe0..3c02f55e5cb7 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,5 +1,5 @@ import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; -import { Root, Range, Thumb, Track } from "@radix-ui/react-slider"; +import { Root as Slider, Range, Thumb, Track } from "@radix-ui/react-slider"; import { useState, useCallback, useMemo } from "react"; import { reconstructLinearGradient, @@ -335,7 +335,7 @@ const SliderThumbComponent = (props: { ); }; -const SliderRoot = styled(Root, { +const SliderRoot = styled(Slider, { position: "relative", width: "100%", height: theme.spacing[9], From 9a9d8cc5e3db04d7208710219b467e1091e4e70c Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 20 Dec 2024 17:27:49 +0530 Subject: [PATCH 09/18] remove popover interaction for color stop change --- .../backgrounds/gradient-control.stories.tsx | 2 +- .../sections/backgrounds/gradient-control.tsx | 108 ++---------------- 2 files changed, 12 insertions(+), 98 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx index 52b52fcd3a37..1556b963a3c1 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -22,7 +22,7 @@ export const GradientWithoutAngle = () => { onChange={(value) => { setGradient(reconstructLinearGradient(value)); }} - onThumbSelected={() => {}} + onThumbSelected={(index, stop) => {}} /> {gradient} diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 3c02f55e5cb7..eab63451f6f6 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,24 +1,15 @@ import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; import { Root as Slider, Range, Thumb, Track } from "@radix-ui/react-slider"; -import { useState, useCallback, useMemo } from "react"; +import { useState, useCallback } from "react"; import { reconstructLinearGradient, type GradientStop, type ParsedGradient, } from "@webstudio-is/css-data"; -import { - styled, - theme, - Flex, - Popover, - PopoverContent, - PopoverTrigger, - Box, -} from "@webstudio-is/design-system"; +import { styled, theme, Flex, Box } from "@webstudio-is/design-system"; import { ChevronBigUpIcon } from "@webstudio-is/icons"; import { colord, extend } from "colord"; import mixPlugin from "colord/plugins/mix"; -import { RgbaColorPicker, type RgbColor } from "react-colorful"; extend([mixPlugin]); @@ -126,8 +117,8 @@ export const GradientControl = (props: GradientControlProps) => { return; } - event.preventDefault(); // Adding a new stop when user clicks on the slider. + event.preventDefault(); const newStopIndex = positions.findIndex( (position) => position > newPosition ); @@ -176,20 +167,6 @@ export const GradientControl = (props: GradientControlProps) => { [props] ); - const handleStopColorChange = useCallback( - (color: RgbValue, stopIndex: number) => { - const newStops = stops; - newStops[stopIndex].color = color; - setStops(newStops); - props.onChange({ - angle: props.gradient.angle, - stops: newStops, - sideOrCorner: props.gradient.sideOrCorner, - }); - }, - [stops, props] - ); - const handleMouseEnter = (event: React.MouseEvent) => { const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(event); setIsHoveredOnStop(isStopExistingAtPosition); @@ -229,13 +206,15 @@ export const GradientControl = (props: GradientControlProps) => { } return ( - + style={{ + background: toValue(stop.color), + }} + onClick={() => handleStopSelected(index, stop)} + > + + ); })} @@ -270,71 +249,6 @@ export const GradientControl = (props: GradientControlProps) => { ); }; -const SliderThumbComponent = (props: { - index: number; - stop: GradientStop; - onSelected: (index: number, stop: GradientStop) => void; - onColorChange: (color: RgbValue, index: number) => void; -}) => { - const { index, stop, onSelected } = props; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const value = useMemo( - () => colord(toValue(stop.color)).toRgb(), - [stop.color] - ); - - const handleClick = useCallback( - (event: React.MouseEvent) => { - if (event.detail === 1) { - onSelected(index, stop); - } - - if (event.detail === 2) { - setIsPopoverOpen(!isPopoverOpen); - } - }, - [index, stop, onSelected, isPopoverOpen] - ); - - const handleOnColorChange = (color: RgbColor) => { - const colordInstance = colord(color).toRgb(); - props.onColorChange( - { - type: "rgb", - alpha: colordInstance.a, - r: color.r, - g: color.g, - b: color.b, - }, - index - ); - }; - - return ( - - - - - - - event.stopPropagation()} - onMouseLeave={() => setIsPopoverOpen(false)} - onClick={(event) => event.stopPropagation()} - onChange={handleOnColorChange} - /> - - - - ); -}; - const SliderRoot = styled(Slider, { position: "relative", width: "100%", From 432830387b06ac25287df983dd5c22acd82d4e50 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 20 Dec 2024 17:30:38 +0530 Subject: [PATCH 10/18] update the slider import from @radix-ui/slider --- .../style-panel/sections/backgrounds/gradient-control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index eab63451f6f6..3d28b1d2ed18 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -1,5 +1,5 @@ import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; -import { Root as Slider, Range, Thumb, Track } from "@radix-ui/react-slider"; +import { Slider, Range, Thumb, Track } from "@radix-ui/react-slider"; import { useState, useCallback } from "react"; import { reconstructLinearGradient, From 8caa73d06e1de51ffd177dee57a5625333700d2c Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 20 Dec 2024 20:55:10 +0530 Subject: [PATCH 11/18] fix ts typechecks --- .../sections/backgrounds/gradient-control.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx index 1556b963a3c1..52b52fcd3a37 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx @@ -22,7 +22,7 @@ export const GradientWithoutAngle = () => { onChange={(value) => { setGradient(reconstructLinearGradient(value)); }} - onThumbSelected={(index, stop) => {}} + onThumbSelected={() => {}} /> {gradient} From f7f35c35775e4b5cb46fc574cd36582683a872d3 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Thu, 2 Jan 2025 20:35:52 +0530 Subject: [PATCH 12/18] update @radix-ui/react-slider --- apps/builder/package.json | 2 +- pnpm-lock.yaml | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/builder/package.json b/apps/builder/package.json index 8c7154bcf42b..cd2172be14d2 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -41,7 +41,7 @@ "@lezer/css": "^1.1.9", "@lezer/highlight": "^1.2.1", "@nanostores/react": "^0.8.0", - "@radix-ui/react-slider": "^1.2.1", + "@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-tooltip": "^1.1.6", "@react-aria/interactions": "^3.22.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f03823df675..fac76d384886 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,9 @@ importers: '@radix-ui/react-select': specifier: ^2.1.4 version: 2.1.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-slider': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-tooltip': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) @@ -3986,8 +3989,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.2.1': - resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==} + '@radix-ui/react-slider@1.2.2': + resolution: {integrity: sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==} peerDependencies: '@types/react': ^18.2.70 '@types/react-dom': ^18.2.25 @@ -10577,15 +10580,15 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 - '@radix-ui/react-slider@1.2.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)': + '@radix-ui/react-slider@1.2.2(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318)': dependencies: '@radix-ui/number': 1.1.0 - '@radix-ui/primitive': 1.1.0 - '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-context': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-direction': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) From 2730ef7db93fc10c36303b7292b57bb219559d33 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Fri, 3 Jan 2025 09:44:35 +0530 Subject: [PATCH 13/18] add chevronbigiconup svg for color hints --- packages/icons/icons/chevron-big-up.svg | 3 +++ .../icons/src/__generated__/components.tsx | 22 +++++++++++++++++++ packages/icons/src/__generated__/svg.ts | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 packages/icons/icons/chevron-big-up.svg diff --git a/packages/icons/icons/chevron-big-up.svg b/packages/icons/icons/chevron-big-up.svg new file mode 100644 index 000000000000..11eb8b43a84f --- /dev/null +++ b/packages/icons/icons/chevron-big-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/__generated__/components.tsx b/packages/icons/src/__generated__/components.tsx index 7a5c7ebe4f31..78d57ddeb001 100644 --- a/packages/icons/src/__generated__/components.tsx +++ b/packages/icons/src/__generated__/components.tsx @@ -1522,6 +1522,28 @@ export const CheckboxCheckedIcon: IconComponent = forwardRef( ); CheckboxCheckedIcon.displayName = "CheckboxCheckedIcon"; +export const ChevronBigUpIcon: IconComponent = forwardRef( + ({ fill = "none", size = 16, ...props }, forwardedRef) => { + return ( + + + + ); + } +); +ChevronBigUpIcon.displayName = "ChevronBigUpIcon"; + export const ChevronDownIcon: IconComponent = forwardRef( ({ fill = "none", size = 16, ...props }, forwardedRef) => { return ( diff --git a/packages/icons/src/__generated__/svg.ts b/packages/icons/src/__generated__/svg.ts index a291aa947d4d..192070077584 100644 --- a/packages/icons/src/__generated__/svg.ts +++ b/packages/icons/src/__generated__/svg.ts @@ -112,6 +112,8 @@ export const CheckMarkIcon = ``; +export const ChevronBigUpIcon = ``; + export const ChevronDownIcon = ``; export const ChevronLeftIcon = ``; From d1dd551a1e4e21ac105f92d4f1293adc8ee7cdc6 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Jan 2025 10:06:21 +0000 Subject: [PATCH 14/18] update icons, added filled chevron --- .../sections/backgrounds/gradient-control.tsx | 4 +-- packages/icons/icons/chevron-big-up.svg | 3 --- packages/icons/icons/chevron-filled-up.svg | 9 +++++++ packages/icons/icons/chevron-up.svg | 17 ++++++++++--- .../icons/src/__generated__/components.tsx | 25 ++++++++----------- packages/icons/src/__generated__/svg.ts | 6 ++--- 6 files changed, 37 insertions(+), 27 deletions(-) delete mode 100644 packages/icons/icons/chevron-big-up.svg create mode 100644 packages/icons/icons/chevron-filled-up.svg diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 3d28b1d2ed18..338bfe7a0447 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -7,9 +7,9 @@ import { type ParsedGradient, } from "@webstudio-is/css-data"; import { styled, theme, Flex, Box } from "@webstudio-is/design-system"; -import { ChevronBigUpIcon } from "@webstudio-is/icons"; import { colord, extend } from "colord"; import mixPlugin from "colord/plugins/mix"; +import { ChevronFilledUpIcon } from "@webstudio-is/icons"; extend([mixPlugin]); @@ -240,7 +240,7 @@ export const GradientControl = (props: GradientControlProps) => { top: theme.spacing[9], }} > - + ); })} diff --git a/packages/icons/icons/chevron-big-up.svg b/packages/icons/icons/chevron-big-up.svg deleted file mode 100644 index 11eb8b43a84f..000000000000 --- a/packages/icons/icons/chevron-big-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/icons/icons/chevron-filled-up.svg b/packages/icons/icons/chevron-filled-up.svg new file mode 100644 index 000000000000..90805644aff1 --- /dev/null +++ b/packages/icons/icons/chevron-filled-up.svg @@ -0,0 +1,9 @@ + + + diff --git a/packages/icons/icons/chevron-up.svg b/packages/icons/icons/chevron-up.svg index aef65ef27968..153ea1c2a31a 100644 --- a/packages/icons/icons/chevron-up.svg +++ b/packages/icons/icons/chevron-up.svg @@ -1,5 +1,14 @@ - - - - + + diff --git a/packages/icons/src/__generated__/components.tsx b/packages/icons/src/__generated__/components.tsx index 78d57ddeb001..281325ab5725 100644 --- a/packages/icons/src/__generated__/components.tsx +++ b/packages/icons/src/__generated__/components.tsx @@ -1522,7 +1522,7 @@ export const CheckboxCheckedIcon: IconComponent = forwardRef( ); CheckboxCheckedIcon.displayName = "CheckboxCheckedIcon"; -export const ChevronBigUpIcon: IconComponent = forwardRef( +export const ChevronDownIcon: IconComponent = forwardRef( ({ fill = "none", size = 16, ...props }, forwardedRef) => { return ( ); } ); -ChevronBigUpIcon.displayName = "ChevronBigUpIcon"; +ChevronDownIcon.displayName = "ChevronDownIcon"; -export const ChevronDownIcon: IconComponent = forwardRef( +export const ChevronFilledUpIcon: IconComponent = forwardRef( ({ fill = "none", size = 16, ...props }, forwardedRef) => { return ( - + ); } ); -ChevronDownIcon.displayName = "ChevronDownIcon"; +ChevronFilledUpIcon.displayName = "ChevronFilledUpIcon"; export const ChevronLeftIcon: IconComponent = forwardRef( ({ fill = "none", size = 16, ...props }, forwardedRef) => { @@ -1631,12 +1628,10 @@ export const ChevronUpIcon: IconComponent = forwardRef( ref={forwardedRef} > ); diff --git a/packages/icons/src/__generated__/svg.ts b/packages/icons/src/__generated__/svg.ts index 192070077584..18250a224f2b 100644 --- a/packages/icons/src/__generated__/svg.ts +++ b/packages/icons/src/__generated__/svg.ts @@ -112,15 +112,15 @@ export const CheckMarkIcon = ``; -export const ChevronBigUpIcon = ``; - export const ChevronDownIcon = ``; +export const ChevronFilledUpIcon = ``; + export const ChevronLeftIcon = ``; export const ChevronRightIcon = ``; -export const ChevronUpIcon = ``; +export const ChevronUpIcon = ``; export const ChevronsLeftIcon = ``; From 8c6ffd92f4effc3ca6b5fc1857d6829ef95235ef Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Jan 2025 10:11:06 +0000 Subject: [PATCH 15/18] use the right border color --- .../style-panel/sections/backgrounds/gradient-control.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 338bfe7a0447..1cec8206b36f 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -253,7 +253,7 @@ const SliderRoot = styled(Slider, { position: "relative", width: "100%", height: theme.spacing[9], - border: `1px solid ${theme.colors.borderInfo}`, + border: `1px solid ${theme.colors.borderMain}`, borderRadius: theme.borderRadius[3], touchAction: "none", userSelect: "none", @@ -284,6 +284,6 @@ const SliderThumb = styled(Thumb, { const SliderThumbTrigger = styled(Box, { width: theme.spacing[9], height: theme.spacing[9], - border: `1px solid ${theme.colors.borderInfo}`, + border: `1px solid ${theme.colors.borderMain}`, borderRadius: theme.borderRadius[3], }); From b00e931b524de53c0c9d32bfe522ffafa6199427 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Jan 2025 11:16:56 +0000 Subject: [PATCH 16/18] design change for handles --- .../sections/backgrounds/gradient-control.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 1cec8206b36f..4c5f11a66b16 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -279,11 +279,22 @@ const SliderThumb = styled(Thumb, { position: "absolute", top: `-${theme.spacing[11]}`, translate: "-9px", + outline: `3px solid ${theme.colors.borderFocus}`, + borderRadius: theme.borderRadius[3], + "&::before": { + content: "''", + position: "absolute", + borderLeft: "5px solid transparent", + borderRight: "5px solid transparent", + borderTop: `5px solid ${theme.colors.borderFocus}`, + bottom: -7, + marginLeft: "50%", + transform: "translateX(-50%)", + borderRadius: 1, + }, }); const SliderThumbTrigger = styled(Box, { - width: theme.spacing[9], - height: theme.spacing[9], - border: `1px solid ${theme.colors.borderMain}`, - borderRadius: theme.borderRadius[3], + width: theme.spacing[7], + height: theme.spacing[7], }); From ca9043ad31a37e74eb37225761b0e3d8e0366cc3 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Jan 2025 16:42:07 +0000 Subject: [PATCH 17/18] design changes --- .../sections/backgrounds/gradient-control.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 4c5f11a66b16..95c1ef5b9389 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -180,8 +180,7 @@ export const GradientControl = (props: GradientControlProps) => { { onMouseMove={handleMouseEnter} onMouseLeave={() => setIsHoveredOnStop(false)} > - - + + {stops.map((stop, index) => { if (stop.color === undefined || stop.position === undefined) { @@ -276,25 +275,26 @@ const SliderRange = styled(Range, { }); const SliderThumb = styled(Thumb, { - position: "absolute", - top: `-${theme.spacing[11]}`, - translate: "-9px", + display: "block", + transform: `translateY(calc(-1 * ${theme.spacing[9]} - 10px))`, + cursor: "move", outline: `3px solid ${theme.colors.borderFocus}`, - borderRadius: theme.borderRadius[3], + borderRadius: theme.borderRadius[5], + outlineOffset: -3, + "&::before": { content: "''", position: "absolute", borderLeft: "5px solid transparent", borderRight: "5px solid transparent", borderTop: `5px solid ${theme.colors.borderFocus}`, - bottom: -7, + bottom: -5, marginLeft: "50%", transform: "translateX(-50%)", - borderRadius: 1, }, }); const SliderThumbTrigger = styled(Box, { - width: theme.spacing[7], - height: theme.spacing[7], + width: theme.spacing[10], + height: theme.spacing[10], }); From 136f21cda86adf9cd19e4c9144239e6717a87e9a Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Jan 2025 17:02:02 +0000 Subject: [PATCH 18/18] remove experimental cursor --- .../style-panel/sections/backgrounds/gradient-control.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx index 95c1ef5b9389..d24b57baf992 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx @@ -277,7 +277,6 @@ const SliderRange = styled(Range, { const SliderThumb = styled(Thumb, { display: "block", transform: `translateY(calc(-1 * ${theme.spacing[9]} - 10px))`, - cursor: "move", outline: `3px solid ${theme.colors.borderFocus}`, borderRadius: theme.borderRadius[5], outlineOffset: -3,