From 12d80025fc7d57a45ed975609aa3c4e1f519e431 Mon Sep 17 00:00:00 2001 From: Jaya Krishna Date: Tue, 24 Sep 2024 18:31:23 +0530 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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, From e903ec96cd9b1fa543c51273e64be5066f54ef6e Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 19:26:37 +0000 Subject: [PATCH 19/24] make it work again --- apps/builder/package.json | 1 + packages/css-data/src/index.ts | 1 + pnpm-lock.yaml | 99 +++++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/apps/builder/package.json b/apps/builder/package.json index beb71c1af12d..fbe11afe3020 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -46,6 +46,7 @@ "@nanostores/react": "^0.8.0", "@nkzw/use-relative-time": "^1.1.0", "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-tooltip": "^1.2.4", "@react-aria/interactions": "^3.23.0", "@react-aria/utils": "^3.27.0", diff --git a/packages/css-data/src/index.ts b/packages/css-data/src/index.ts index 81f06b58103f..b11d0d27ce97 100644 --- a/packages/css-data/src/index.ts +++ b/packages/css-data/src/index.ts @@ -8,6 +8,7 @@ export { } from "./__generated__/property-value-descriptions"; export * from "./__generated__/animatable-properties"; export * from "./__generated__/pseudo-elements"; +export * from "./property-parsers"; // shorthand property parsers export * from "./parse-css-value"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bac6d81e9111..d77fe287cc98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: '@radix-ui/react-select': specifier: ^2.2.2 version: 2.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-slider': + specifier: ^1.3.6 + version: 1.3.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) '@radix-ui/react-tooltip': specifier: ^1.2.4 version: 1.2.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) @@ -4105,6 +4108,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-accessible-icon@1.1.4': resolution: {integrity: sha512-J8pIt7l32A9fGIn86vwccQzik5MgIOTtceeTxi6EiiFYwWHLxsTHwiOW4pI5sQhQJWd3MOEkumFBIHwIU038Cw==} peerDependencies: @@ -4196,6 +4202,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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-compose-refs@1.0.1': resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -4494,6 +4513,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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-progress@1.1.4': resolution: {integrity: sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==} peerDependencies: @@ -4572,8 +4604,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.2.2': - resolution: {integrity: sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==} + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} peerDependencies: '@types/react': ^18.2.70 '@types/react-dom': ^18.2.25 @@ -4612,6 +4644,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': ^18.2.70 + react: 18.3.0-canary-14898b6a9-20240318 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.2.2': resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==} peerDependencies: @@ -11227,6 +11268,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-accessible-icon@1.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)': dependencies: '@radix-ui/react-visually-hidden': 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) @@ -11319,6 +11362,18 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 + '@radix-ui/react-collection@1.1.7(@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/react-compose-refs': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-primitive': 2.1.3(@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-slot': 1.2.3(@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-compose-refs@1.0.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318)': dependencies: '@babel/runtime': 7.22.3 @@ -11616,6 +11671,15 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 + '@radix-ui/react-primitive@2.1.3(@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/react-slot': 1.2.3(@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-progress@1.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)': dependencies: '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) @@ -11717,19 +11781,19 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 - '@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)': + '@radix-ui/react-slider@1.3.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)': dependencies: - '@radix-ui/number': 1.1.0 - '@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.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) - '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@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.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-direction': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-primitive': 2.1.3(@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.2.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + '@radix-ui/react-use-size': 1.1.1(@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: @@ -11758,6 +11822,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.79 + '@radix-ui/react-slot@1.2.3(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) + react: 18.3.0-canary-14898b6a9-20240318 + optionalDependencies: + '@types/react': 18.2.79 + '@radix-ui/react-switch@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/primitive': 1.1.2 From 712f1e11366f4dc4edbb5d96923f2b5260c0b791 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 21:17:51 +0000 Subject: [PATCH 20/24] remove react-slider --- .../sections/backgrounds/gradient-control.tsx | 455 +++++++++++++----- apps/builder/package.json | 1 - pnpm-lock.yaml | 103 ---- 3 files changed, 322 insertions(+), 237 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 d24b57baf992..0bafb6604748 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,6 +1,12 @@ import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; -import { Slider, Range, Thumb, Track } from "@radix-ui/react-slider"; -import { useState, useCallback } from "react"; +import { + useState, + useCallback, + useRef, + type KeyboardEvent as ReactKeyboardEvent, + type MouseEvent as ReactMouseEvent, + type PointerEvent as ReactPointerEvent, +} from "react"; import { reconstructLinearGradient, type GradientStop, @@ -25,155 +31,327 @@ const defaultAngle: UnitValue = { unit: "deg", }; +const clamp = (value: number, min = 0, max = 100) => + Math.min(Math.max(value, min), max); + +const THUMB_INTERACTION_PX = 12; + export const GradientControl = (props: GradientControlProps) => { - const [stops, setStops] = useState>(props.gradient.stops); + const { gradient, onChange, onThumbSelected } = props; + const [stops, setStops] = useState>(gradient.stops); const [selectedStop, setSelectedStop] = useState(); const [isHoveredOnStop, setIsHoveredOnStop] = useState(false); + const sliderRef = useRef(null); + const positions = stops .map((stop) => stop.position?.value) - .filter((item) => item !== undefined); - const hints = props.gradient.stops + .filter((item): item is number => item !== undefined); + const hints = gradient.stops .map((stop) => stop.hint?.value) - .filter((item) => item !== undefined); + .filter((item): item is number => item !== undefined); const background = reconstructLinearGradient({ stops, - sideOrCorner: props.gradient.sideOrCorner, + sideOrCorner: gradient.sideOrCorner, angle: defaultAngle, }); - // 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 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 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 - ); - - 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: newStops, - sideOrCorner: props.gradient.sideOrCorner, + const updateStops = useCallback( + (updater: (currentStops: GradientStop[]) => GradientStop[]) => { + setStops((currentStops) => { + const nextStops = updater(currentStops); + onChange({ + angle: gradient.angle, + stops: nextStops, + sideOrCorner: gradient.sideOrCorner, + }); + return nextStops; }); }, - [stops, props] + [gradient.angle, gradient.sideOrCorner, onChange] ); - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "Backspace" && selectedStop !== undefined) { - const newStops = stops; - newStops.splice(selectedStop, 1); - setStops(newStops); - setSelectedStop(undefined); - } + const updateStopPosition = useCallback( + (index: number, value: number) => { + const nextValue = clamp(value); + updateStops((currentStops) => { + if (index < 0 || index >= currentStops.length) { + return currentStops; + } + + return currentStops.map((stop, stopIndex) => { + if (stopIndex !== index) { + return stop; + } + + const nextPosition = { + type: "unit", + unit: "%", + value: nextValue, + } as const; + + if (stop.position === undefined) { + return { + ...stop, + position: nextPosition, + }; + } + + return { + ...stop, + position: nextPosition, + }; + }); + }); }, - [stops, selectedStop] + [updateStops] ); + const computePositionFromClientX = useCallback((clientX: number) => { + const rect = sliderRef.current?.getBoundingClientRect(); + if (rect === undefined || rect.width === 0) { + return 0; + } + const relativePosition = clientX - rect.left; + return clamp(Math.round((relativePosition / rect.width) * 100)); + }, []); + const checkIfStopExistsAtPosition = 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); - // 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 - ); + clientX: number + ): { + isStopExistingAtPosition: boolean; + newPosition: number; + } => { + const rect = sliderRef.current?.getBoundingClientRect(); + const newPosition = computePositionFromClientX(clientX); + + if (rect === undefined || rect.width === 0) { + return { isStopExistingAtPosition: false, newPosition }; + } + + const relativeX = clamp(clientX - rect.left, 0, rect.width); + const isStopExistingAtPosition = positions.some((position) => { + const positionPx = (position / 100) * rect.width; + return Math.abs(positionPx - relativeX) <= THUMB_INTERACTION_PX; + }); return { isStopExistingAtPosition, newPosition }; }, - [positions] + [computePositionFromClientX, positions] + ); + + const handleStopSelected = useCallback( + (index: number, stop: GradientStop) => { + setSelectedStop(index); + onThumbSelected(index, stop); + }, + [onThumbSelected] + ); + + const handleThumbPointerDown = useCallback( + (index: number, stop: GradientStop) => + (event: ReactPointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + + handleStopSelected(index, stop); + setIsHoveredOnStop(true); + + const pointerId = event.pointerId; + const target = event.currentTarget; + target.setPointerCapture(pointerId); + + const handlePointerMove = (moveEvent: PointerEvent) => { + const newPosition = computePositionFromClientX(moveEvent.clientX); + updateStopPosition(index, newPosition); + }; + + const handlePointerUp = () => { + target.releasePointerCapture(pointerId); + target.removeEventListener("pointermove", handlePointerMove); + target.removeEventListener("pointerup", handlePointerUp); + target.removeEventListener("pointercancel", handlePointerUp); + setIsHoveredOnStop(false); + }; + + target.addEventListener("pointermove", handlePointerMove); + target.addEventListener("pointerup", handlePointerUp); + target.addEventListener("pointercancel", handlePointerUp); + }, + [computePositionFromClientX, handleStopSelected, updateStopPosition] + ); + + const handleKeyDown = useCallback( + (event: ReactKeyboardEvent) => { + if (selectedStop === undefined) { + return; + } + + if (event.key === "Backspace") { + event.preventDefault(); + let nextSelection: + | { index: number | undefined; stop?: GradientStop } + | undefined; + + updateStops((currentStops) => { + if (selectedStop < 0 || selectedStop >= currentStops.length) { + return currentStops; + } + + const nextStops = currentStops.filter( + (_, index) => index !== selectedStop + ); + + if (nextStops.length > 0) { + const candidateIndex = Math.min(selectedStop, nextStops.length - 1); + nextSelection = { + index: candidateIndex, + stop: nextStops[candidateIndex], + }; + } else { + nextSelection = { index: undefined }; + } + + return nextStops; + }); + + if (nextSelection?.index !== undefined && nextSelection.stop) { + setSelectedStop(nextSelection.index); + onThumbSelected(nextSelection.index, nextSelection.stop); + } else { + setSelectedStop(undefined); + } + + return; + } + + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + event.preventDefault(); + const step = event.shiftKey ? 10 : 1; + const delta = event.key === "ArrowLeft" ? -step : step; + const currentPosition = stops[selectedStop]?.position?.value ?? 0; + updateStopPosition(selectedStop, currentPosition + delta); + } + }, + [onThumbSelected, selectedStop, stops, updateStopPosition, updateStops] ); const handlePointerDown = useCallback( - (event: React.MouseEvent) => { - if (event.target === undefined || event.target === null) { + (event: ReactPointerEvent) => { + if ( + event.target instanceof HTMLElement && + event.target.closest("[data-thumb='true']") + ) { 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 } = - checkIfStopExistsAtPosition(event); + checkIfStopExistsAtPosition(event.clientX); if (isStopExistingAtPosition === true) { return; } - // Adding a new stop when user clicks on the slider. 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), - { + + let nextSelection: { index: number; stop: GradientStop } | undefined; + + updateStops((currentStops) => { + if (currentStops.length === 0) { + return currentStops; + } + + const currentPositions = currentStops + .map((stop) => stop.position?.value) + .filter((value): value is number => value !== undefined); + + const newStopIndex = currentPositions.findIndex( + (position) => position > newPosition + ); + const insertionIndex = + newStopIndex === -1 ? currentStops.length : newStopIndex; + + const prevIndex = insertionIndex === 0 ? 0 : insertionIndex - 1; + const nextIndex = + insertionIndex === currentStops.length + ? currentStops.length - 1 + : insertionIndex; + + const prevColor = currentStops[prevIndex]?.color; + const nextColor = currentStops[nextIndex]?.color ?? prevColor; + + if (prevColor === undefined && nextColor === undefined) { + return currentStops; + } + + const interpolationColor = + prevColor !== undefined && nextColor !== undefined + ? colord(toValue(prevColor)) + .mix(colord(toValue(nextColor)), newPosition / 100) + .toRgb() + : colord(toValue((prevColor ?? nextColor)!)).toRgb(); + + const newColorStop: RgbValue = { + type: "rgb", + alpha: interpolationColor.a, + r: interpolationColor.r, + g: interpolationColor.g, + b: interpolationColor.b, + }; + + const newStop: GradientStop = { color: newColorStop, position: { type: "unit", value: newPosition, unit: "%" }, - }, - ...stops.slice(index), - ]; + }; - setStops(newStops); - setIsHoveredOnStop(true); - props.onChange({ - angle: props.gradient.angle, - stops: newStops, - sideOrCorner: props.gradient.sideOrCorner, + const nextStops: GradientStop[] = [ + ...currentStops.slice(0, insertionIndex), + newStop, + ...currentStops.slice(insertionIndex), + ]; + + nextSelection = { index: insertionIndex, stop: newStop }; + + return nextStops; }); + + if (nextSelection !== undefined) { + setSelectedStop(nextSelection.index); + onThumbSelected(nextSelection.index, nextSelection.stop); + } + + setIsHoveredOnStop(true); }, - [stops, positions, checkIfStopExistsAtPosition, props] + [checkIfStopExistsAtPosition, onThumbSelected, updateStops] ); - const handleStopSelected = useCallback( - (index: number, stop: GradientStop) => { - setSelectedStop(index); - props.onThumbSelected(index, stop); + const handleMouseIndicator = useCallback( + (event: ReactMouseEvent) => { + const { isStopExistingAtPosition } = checkIfStopExistsAtPosition( + event.clientX + ); + setIsHoveredOnStop(isStopExistingAtPosition); }, - [props] + [checkIfStopExistsAtPosition] ); - const handleMouseEnter = (event: React.MouseEvent) => { - const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(event); - setIsHoveredOnStop(isStopExistingAtPosition); - }; + const handleSliderFocus = useCallback(() => { + if (selectedStop !== undefined || stops.length === 0) { + return; + } - if (isEveryStopHasAPosition === false) { - return; + const [firstStop] = stops; + if (firstStop !== undefined) { + handleStopSelected(0, firstStop); + } + }, [handleStopSelected, selectedStop, stops]); + + if ( + stops.some( + (stop) => stop.position === undefined || stop.color === undefined + ) + ) { + return null; } return ( @@ -184,49 +362,48 @@ export const GradientControl = (props: GradientControlProps) => { }} > setIsHoveredOnStop(false)} > - - - + {stops.map((stop, index) => { if (stop.color === undefined || stop.position === undefined) { - return; + return null; } return ( handleStopSelected(index, stop)} > - + ); })} - {/* - 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 with stops if they managed to add gradient from the advanced tab. - */} {hints.map((hint) => { return ( { ); }; -const SliderRoot = styled(Slider, { +const SliderRoot = styled("div", { position: "relative", width: "100%", height: theme.spacing[9], @@ -256,6 +433,7 @@ const SliderRoot = styled(Slider, { borderRadius: theme.borderRadius[3], touchAction: "none", userSelect: "none", + outline: "none", variants: { isHoveredOnStop: { true: { @@ -266,21 +444,30 @@ const SliderRoot = styled(Slider, { }, }, }, + "&:focus-visible": { + boxShadow: `0 0 0 2px ${theme.colors.borderFocus}`, + }, }); -const SliderRange = styled(Range, { +const SliderTrack = styled("div", { position: "absolute", - background: "transparent", + inset: 0, borderRadius: theme.borderRadius[3], + pointerEvents: "none", }); -const SliderThumb = styled(Thumb, { +const SliderThumb = styled(Box, { + position: "absolute", display: "block", - transform: `translateY(calc(-1 * ${theme.spacing[9]} - 10px))`, + transform: `translate(-50%, calc(-1 * ${theme.spacing[9]} - 10px))`, outline: `3px solid ${theme.colors.borderFocus}`, borderRadius: theme.borderRadius[5], outlineOffset: -3, - + zIndex: 1, + cursor: "grab", + "&:active": { + cursor: "grabbing", + }, "&::before": { content: "''", position: "absolute", @@ -296,4 +483,6 @@ const SliderThumb = styled(Thumb, { const SliderThumbTrigger = styled(Box, { width: theme.spacing[10], height: theme.spacing[10], + borderRadius: theme.borderRadius[4], + backgroundColor: "inherit", }); diff --git a/apps/builder/package.json b/apps/builder/package.json index fbe11afe3020..beb71c1af12d 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -46,7 +46,6 @@ "@nanostores/react": "^0.8.0", "@nkzw/use-relative-time": "^1.1.0", "@radix-ui/react-select": "^2.2.2", - "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-tooltip": "^1.2.4", "@react-aria/interactions": "^3.23.0", "@react-aria/utils": "^3.27.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d77fe287cc98..ba2a74f2bc6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,9 +199,6 @@ importers: '@radix-ui/react-select': specifier: ^2.2.2 version: 2.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-slider': - specifier: ^1.3.6 - version: 1.3.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) '@radix-ui/react-tooltip': specifier: ^1.2.4 version: 1.2.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) @@ -4108,9 +4105,6 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - '@radix-ui/react-accessible-icon@1.1.4': resolution: {integrity: sha512-J8pIt7l32A9fGIn86vwccQzik5MgIOTtceeTxi6EiiFYwWHLxsTHwiOW4pI5sQhQJWd3MOEkumFBIHwIU038Cw==} peerDependencies: @@ -4202,19 +4196,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} - 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-compose-refs@1.0.1': resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -4513,19 +4494,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - 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-progress@1.1.4': resolution: {integrity: sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==} peerDependencies: @@ -4604,19 +4572,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.3.6': - resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} - 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: @@ -4644,15 +4599,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': ^18.2.70 - react: 18.3.0-canary-14898b6a9-20240318 - peerDependenciesMeta: - '@types/react': - optional: true - '@radix-ui/react-switch@1.2.2': resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==} peerDependencies: @@ -11268,8 +11214,6 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accessible-icon@1.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)': dependencies: '@radix-ui/react-visually-hidden': 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) @@ -11362,18 +11306,6 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 - '@radix-ui/react-collection@1.1.7(@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/react-compose-refs': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-primitive': 2.1.3(@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-slot': 1.2.3(@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-compose-refs@1.0.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318)': dependencies: '@babel/runtime': 7.22.3 @@ -11671,15 +11603,6 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 - '@radix-ui/react-primitive@2.1.3(@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/react-slot': 1.2.3(@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-progress@1.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)': dependencies: '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) @@ -11781,25 +11704,6 @@ snapshots: '@types/react': 18.2.79 '@types/react-dom': 18.2.25 - '@radix-ui/react-slider@1.3.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)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@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.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-context': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-direction': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-primitive': 2.1.3(@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.2.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-use-size': 1.1.1(@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 @@ -11822,13 +11726,6 @@ snapshots: optionalDependencies: '@types/react': 18.2.79 - '@radix-ui/react-slot@1.2.3(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) - react: 18.3.0-canary-14898b6a9-20240318 - optionalDependencies: - '@types/react': 18.2.79 - '@radix-ui/react-switch@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/primitive': 1.1.2 From 560ce54d6166fac54e5ebf9f20f4df441eab9c89 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 22:10:16 +0000 Subject: [PATCH 21/24] move to design system --- packages/design-system/package.json | 3 +++ .../src/components}/gradient-control.stories.tsx | 6 +++--- .../src/components}/gradient-control.tsx | 12 ++++++++---- packages/design-system/src/index.ts | 1 + pnpm-lock.yaml | 9 +++++++++ 5 files changed, 24 insertions(+), 7 deletions(-) rename {apps/builder/app/builder/features/style-panel/sections/backgrounds => packages/design-system/src/components}/gradient-control.stories.tsx (96%) rename {apps/builder/app/builder/features/style-panel/sections/backgrounds => packages/design-system/src/components}/gradient-control.tsx (98%) diff --git a/packages/design-system/package.json b/packages/design-system/package.json index fce70513308f..5c4631df9bc3 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -58,7 +58,10 @@ "@react-aria/interactions": "^3.23.0", "@react-aria/utils": "^3.27.0", "@stitches/react": "1.3.1-1", + "@webstudio-is/css-data": "workspace:*", + "@webstudio-is/css-engine": "workspace:*", "@webstudio-is/icons": "workspace:*", + "colord": "^2.9.3", "change-case": "^5.4.4", "cmdk": "^1.1.1", "downshift": "^6.1.7", diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx b/packages/design-system/src/components/gradient-control.stories.tsx similarity index 96% rename from apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx rename to packages/design-system/src/components/gradient-control.stories.tsx index 52b52fcd3a37..b304fc6c12b1 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.stories.tsx +++ b/packages/design-system/src/components/gradient-control.stories.tsx @@ -3,9 +3,10 @@ import { reconstructLinearGradient, type ParsedGradient, } from "@webstudio-is/css-data"; -import { GradientControl } from "./gradient-control"; -import { Flex, Text } from "@webstudio-is/design-system"; import { useState } from "react"; +import { GradientControl } from "./gradient-control"; +import { Flex } from "./flex"; +import { Text } from "./text"; export default { title: "Library/GradientControl", @@ -50,7 +51,6 @@ export const GradientWithAngleAndHints = () => { export const GradientWithSideOrCorner = () => { const gradientString = "linear-gradient(to left top, blue 0%, red 100%)"; - const [gradient, setGradient] = useState(gradientString); return ( diff --git a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx b/packages/design-system/src/components/gradient-control.tsx similarity index 98% rename from apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx rename to packages/design-system/src/components/gradient-control.tsx index 0bafb6604748..52cc51e23964 100644 --- a/apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-control.tsx +++ b/packages/design-system/src/components/gradient-control.tsx @@ -12,10 +12,12 @@ import { type GradientStop, type ParsedGradient, } from "@webstudio-is/css-data"; -import { styled, theme, Flex, Box } from "@webstudio-is/design-system"; import { colord, extend } from "colord"; import mixPlugin from "colord/plugins/mix"; import { ChevronFilledUpIcon } from "@webstudio-is/icons"; +import { styled, theme } from "../stitches.config"; +import { Flex } from "./flex"; +import { Box } from "./box"; extend([mixPlugin]); @@ -31,11 +33,11 @@ const defaultAngle: UnitValue = { unit: "deg", }; +const THUMB_INTERACTION_PX = 12; + const clamp = (value: number, min = 0, max = 100) => Math.min(Math.max(value, min), max); -const THUMB_INTERACTION_PX = 12; - export const GradientControl = (props: GradientControlProps) => { const { gradient, onChange, onThumbSelected } = props; const [stops, setStops] = useState>(gradient.stops); @@ -47,7 +49,7 @@ export const GradientControl = (props: GradientControlProps) => { .map((stop) => stop.position?.value) .filter((item): item is number => item !== undefined); const hints = gradient.stops - .map((stop) => stop.hint?.value) + .map((stop): number | undefined => stop.hint?.value) .filter((item): item is number => item !== undefined); const background = reconstructLinearGradient({ stops, @@ -486,3 +488,5 @@ const SliderThumbTrigger = styled(Box, { borderRadius: theme.borderRadius[4], backgroundColor: "inherit", }); + +export type { GradientControlProps }; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index 45056946c7f9..223350506dd3 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -48,6 +48,7 @@ export * from "./components/panel-banner"; export * from "./components/focus-ring"; export * from "./components/tree"; export * from "./components/command"; +export * from "./components/gradient-control"; // Not aligned diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba2a74f2bc6a..d346e86ff188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1426,6 +1426,12 @@ importers: '@stitches/react': specifier: 1.3.1-1 version: 1.3.1-1(patch_hash=knml42mpr6mlzseo3d6gjljiq4)(react@18.3.0-canary-14898b6a9-20240318) + '@webstudio-is/css-data': + specifier: workspace:* + version: link:../css-data + '@webstudio-is/css-engine': + specifier: workspace:* + version: link:../css-engine '@webstudio-is/icons': specifier: workspace:* version: link:../icons @@ -1435,6 +1441,9 @@ importers: cmdk: specifier: ^1.1.1 version: 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) + colord: + specifier: ^2.9.3 + version: 2.9.3 downshift: specifier: ^6.1.7 version: 6.1.7(react@18.3.0-canary-14898b6a9-20240318) From b365d32dc6bc79a8a462dbe4772ebd7b61129023 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 22:30:32 +0000 Subject: [PATCH 22/24] move --- ...control.stories.tsx => gradient-picker.stories.tsx} | 10 +++++----- .../{gradient-control.tsx => gradient-picker.tsx} | 6 +++--- packages/design-system/src/index.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/design-system/src/components/{gradient-control.stories.tsx => gradient-picker.stories.tsx} (91%) rename packages/design-system/src/components/{gradient-control.tsx => gradient-picker.tsx} (99%) diff --git a/packages/design-system/src/components/gradient-control.stories.tsx b/packages/design-system/src/components/gradient-picker.stories.tsx similarity index 91% rename from packages/design-system/src/components/gradient-control.stories.tsx rename to packages/design-system/src/components/gradient-picker.stories.tsx index b304fc6c12b1..445bf761aac2 100644 --- a/packages/design-system/src/components/gradient-control.stories.tsx +++ b/packages/design-system/src/components/gradient-picker.stories.tsx @@ -4,12 +4,12 @@ import { type ParsedGradient, } from "@webstudio-is/css-data"; import { useState } from "react"; -import { GradientControl } from "./gradient-control"; +import { GradientPicker } from "./gradient-picker"; import { Flex } from "./flex"; import { Text } from "./text"; export default { - title: "Library/GradientControl", + title: "Library/GradientPicker", }; export const GradientWithoutAngle = () => { @@ -18,7 +18,7 @@ export const GradientWithoutAngle = () => { return ( - { setGradient(reconstructLinearGradient(value)); @@ -37,7 +37,7 @@ export const GradientWithAngleAndHints = () => { return ( - { setGradient(reconstructLinearGradient(value)); @@ -55,7 +55,7 @@ export const GradientWithSideOrCorner = () => { return ( - { setGradient(reconstructLinearGradient(value)); diff --git a/packages/design-system/src/components/gradient-control.tsx b/packages/design-system/src/components/gradient-picker.tsx similarity index 99% rename from packages/design-system/src/components/gradient-control.tsx rename to packages/design-system/src/components/gradient-picker.tsx index 52cc51e23964..7937650fd67c 100644 --- a/packages/design-system/src/components/gradient-control.tsx +++ b/packages/design-system/src/components/gradient-picker.tsx @@ -21,7 +21,7 @@ import { Box } from "./box"; extend([mixPlugin]); -type GradientControlProps = { +type GradientPickerProps = { gradient: ParsedGradient; onChange: (value: ParsedGradient) => void; onThumbSelected: (index: number, stop: GradientStop) => void; @@ -38,7 +38,7 @@ const THUMB_INTERACTION_PX = 12; const clamp = (value: number, min = 0, max = 100) => Math.min(Math.max(value, min), max); -export const GradientControl = (props: GradientControlProps) => { +export const GradientPicker = (props: GradientPickerProps) => { const { gradient, onChange, onThumbSelected } = props; const [stops, setStops] = useState>(gradient.stops); const [selectedStop, setSelectedStop] = useState(); @@ -489,4 +489,4 @@ const SliderThumbTrigger = styled(Box, { backgroundColor: "inherit", }); -export type { GradientControlProps }; +export type { GradientPickerProps }; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index 223350506dd3..fa609fc9a240 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -48,7 +48,7 @@ export * from "./components/panel-banner"; export * from "./components/focus-ring"; export * from "./components/tree"; export * from "./components/command"; -export * from "./components/gradient-control"; +export * from "./components/gradient-picker"; // Not aligned From 5250439badf32c7eb84d028be741f8210c3955be Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 22:36:47 +0000 Subject: [PATCH 23/24] reuse clamp --- packages/design-system/src/components/gradient-picker.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/design-system/src/components/gradient-picker.tsx b/packages/design-system/src/components/gradient-picker.tsx index 7937650fd67c..456b059d6788 100644 --- a/packages/design-system/src/components/gradient-picker.tsx +++ b/packages/design-system/src/components/gradient-picker.tsx @@ -1,3 +1,4 @@ +import { clamp } from "@react-aria/utils"; import { toValue, UnitValue, type RgbValue } from "@webstudio-is/css-engine"; import { useState, @@ -35,9 +36,6 @@ const defaultAngle: UnitValue = { const THUMB_INTERACTION_PX = 12; -const clamp = (value: number, min = 0, max = 100) => - Math.min(Math.max(value, min), max); - export const GradientPicker = (props: GradientPickerProps) => { const { gradient, onChange, onThumbSelected } = props; const [stops, setStops] = useState>(gradient.stops); @@ -74,7 +72,7 @@ export const GradientPicker = (props: GradientPickerProps) => { const updateStopPosition = useCallback( (index: number, value: number) => { - const nextValue = clamp(value); + const nextValue = clamp(value, 0, 100); updateStops((currentStops) => { if (index < 0 || index >= currentStops.length) { return currentStops; @@ -114,7 +112,7 @@ export const GradientPicker = (props: GradientPickerProps) => { return 0; } const relativePosition = clientX - rect.left; - return clamp(Math.round((relativePosition / rect.width) * 100)); + return clamp(Math.round((relativePosition / rect.width) * 100), 0, 100); }, []); const checkIfStopExistsAtPosition = useCallback( From 0ef22bbdb074ac38ff3a7f654aa1581ce091fe3a Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Thu, 13 Nov 2025 22:40:41 +0000 Subject: [PATCH 24/24] types --- packages/css-data/src/property-parsers/linear-gradient.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/css-data/src/property-parsers/linear-gradient.ts b/packages/css-data/src/property-parsers/linear-gradient.ts index 0c6313ac9317..cc1af7aecd68 100644 --- a/packages/css-data/src/property-parsers/linear-gradient.ts +++ b/packages/css-data/src/property-parsers/linear-gradient.ts @@ -12,17 +12,17 @@ import namesPlugin from "colord/plugins/names"; extend([namesPlugin]); -export interface GradientStop { +export type GradientStop = { color?: RgbValue; position?: UnitValue; hint?: UnitValue; -} +}; -export interface ParsedGradient { +export type ParsedGradient = { angle?: UnitValue; sideOrCorner?: KeywordValue; stops: GradientStop[]; -} +}; const sideOrCorderIdentifiers = ["to", "top", "bottom", "left", "right"];