Skip to content

Commit cfa3827

Browse files
hrhvSwarnimDoegarpiyushrynCopilotCopilot
authored
UX Enhancements and addition of new features to the Editor (#9)
* Support negative values in position x and y for image and text layer * Add support for duplicate and rename in L1 sidebar * feat: add trim transformation support * Add advanced padding input support * Remove unwanted console logs * Fix Icon Button in padding input * Add focus support in image overlay * fix: update trim transformation schema and default values for improved functionality * feat: add color replace transformation with validation and integration into schema * Add tooltip and better UX in padding input * Add zoom support with fo face and object in base image and image overlay * Fix zoom input step size and default value * Fix padding toggle button style * feat: add border and sharpen transformations to image processing schema with validation and integration * refactor: improve transformation schema for border, trim, and sharpen with enhanced validation and default values * Update packages/imagekit-editor-dev/src/schema/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Initial plan * Update packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: correct formatting for items array opening bracket at line 1855 Co-authored-by: piyushryn <105715267+piyushryn@users.noreply.github.com> * Remove unused imports in PaddingInput component * Add gradient effect support in base and overlay images * Improve renaming mode UX * Improve Gradient Picker implementation * Add shadow and grayscale support in image layer * refactor: update borderWidth validation in transformation schema to use union type and improve visibility logic * feat: add unsharpen mask transformation to image processing schema with validation and integration * feat: integrate unsharpen mask transformation into overlay processing logic * feat: enhance transformation schema with new lineHeight and dpr options, and improve validation logic * Retain Padding Input Value * Retain Zoom Input Value * fix: update trimEnabled transformation property to disable trimming and remove debug log * Add Distortion support in base and overlay image * Refactor padding input * Add per corner radius support in base and overlay image * fix: color picker alpha behavior consistent with the downstream service * chore: added .cursor to gitignore * Improved distort perspective implementation * chore: Add VSCode extension recommendations for TypeScript and Biome * chore(workflows): Add lint step to CI and publish * Fix linting issues * Fix type and linting issues * Fix more linting errors * Fixed formatting issues * Do not use structured clone * Fix formatting * Move to strict type * feat: complete ux revamp for backgrounds * feat: resize and crop mode revamp ux * fix: perspective and arc distort validations * feat: use descriptive copy for crop modes inside image layer menu * fix: radius values persistence was lost when reordering or changing views * feat: While duplicating, appending a text "Copy" for better visual separation of the newly created entity * feat: arranging items within the list's transformation section in alphabetical order * fix: dpr can only be used with either width or height, so it is moved inside resize and crop menu * fix: dpr behavior inside image layers * fix: dpr in layers made clearable using switch + lint fixes * fix: opacity and quality settings inside layers are now controlled by a switch to enable clearable value support * fix: used clearable select field for crop mode inside layers + improved copy * feat: color picker component now supports isClearable prop * fix: issues with padding and line height in text layers + added proper validator for line height which validates integer and expression values * fix: opacity in text layers only supports 1-9 integers. * fix: issue where an integer value for radius was losing persistence * release: bump @imagekit/editor version to 2.1.0 --------- Co-authored-by: Swarnim Doegar <swarnim@imagekit.io> Co-authored-by: Piyush Aryan <piyush@imagekit.io> Co-authored-by: Piyush Aryan <105715267+piyushryn@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Swarnim Doegar <swarnimdoegar@gmail.com> Co-authored-by: Abhinav Dhiman <abhinav@imagekit.io>
1 parent 1dff55e commit cfa3827

31 files changed

Lines changed: 5272 additions & 1359 deletions

.github/workflows/ci.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
- name: 📦 Install deps, build, pack
2626
run: |
2727
yarn install --frozen-lockfile
28+
yarn lint
2829
yarn package
2930
env:
3031
CI: true
@@ -33,4 +34,4 @@ jobs:
3334
uses: actions/upload-artifact@v4
3435
with:
3536
name: imagekit-editor-package
36-
path: builds/imagekit-editor-*.tgz
37+
path: builds/imagekit-editor-*.tgz

.github/workflows/node-publish.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ jobs:
2222
with:
2323
node-version: 20.x
2424
cache: yarn
25-
registry-url: 'https://registry.npmjs.org'
25+
registry-url: "https://registry.npmjs.org"
2626

2727
- name: Build and Publish
2828
run: |
2929
yarn install --frozen-lockfile
3030
31+
yarn lint
32+
3133
yarn build
3234
3335
npm whoami
@@ -48,4 +50,4 @@ jobs:
4850
4951
env:
5052
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
51-
CI: true
53+
CI: true

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ packages/imagekit-editor/*.tgz
1818
.turbo
1919
.yarn
2020
builds
21-
packages/imagekit-editor/README.md
21+
packages/imagekit-editor/README.md
22+
.cursor

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["ms-vscode.vscode-typescript-next", "biomejs.biome"]
3+
}

examples/react-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@imagekit/editor": "2.0.0",
6+
"@imagekit/editor": "2.1.0",
77
"@types/node": "^20.11.24",
88
"@types/react": "^17.0.2",
99
"@types/react-dom": "^17.0.2",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"package": "yarn build && shx cp README.md ./packages/imagekit-editor/ && yarn workspace @imagekit/editor pack --out ../../builds/imagekit-editor-%v.tgz",
2121
"release": "yarn build && shx cp README.md ./packages/imagekit-editor/ && yarn workspace @imagekit/editor pack --out ../../builds/imagekit-editor-%v.tgz && yarn workspace @imagekit/editor publish",
2222
"prepare": "husky",
23-
"lint": "biome ci"
23+
"lint": "biome ci",
24+
"lint:fix": "biome format --write ./"
2425
},
2526
"devDependencies": {
2627
"@biomejs/biome": "2.1.1",

packages/imagekit-editor-dev/src/components/RetryableImage.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from "@chakra-ui/react"
1212
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
1313
import { useVisibility } from "../hooks/useVisibility"
14-
import { useEditorStore } from "../store"
1514

1615
export interface RetryableImageProps extends ImageProps {
1716
maxRetries?: number
@@ -105,11 +104,12 @@ export default function RetryableImage(props: RetryableImageProps) {
105104
setProbing(true)
106105
}, [currentSrcBase, src])
107106

107+
// biome-ignore lint/correctness/useExhaustiveDependencies: <not needed>
108108
useEffect(() => {
109109
if (!src) return
110110
if (lazy && !visible) return
111111
setAttempt(0)
112-
beginLoad(0)
112+
beginLoad()
113113
}, [src, visible, lazy])
114114

115115
const scheduleRetry = useCallback(() => {
@@ -156,7 +156,11 @@ export default function RetryableImage(props: RetryableImageProps) {
156156
}
157157

158158
return (
159-
<Box ref={rootRef as any} position="relative" display="inline-block">
159+
<Box
160+
ref={rootRef as React.RefObject<HTMLDivElement>}
161+
position="relative"
162+
display="inline-block"
163+
>
160164
{error ? (
161165
<Center
162166
w={imgProps.width || "full"}

packages/imagekit-editor-dev/src/components/common/AnchorField.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ const AnchorField: React.FC<AnchorFieldProps> = ({
7878
minWidth="0"
7979
p="0"
8080
isDisabled={!positions.includes(position.value)}
81-
onClick={() => onChange(position.value)}
81+
onClick={() => {
82+
if (value === position.value) {
83+
return onChange("")
84+
}
85+
onChange(position.value)
86+
}}
8287
borderRadius="md"
8388
border={
8489
value === position.value

packages/imagekit-editor-dev/src/components/common/CheckboxCardField.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type As,
23
Box,
34
Flex,
45
HStack,
@@ -67,6 +68,7 @@ export const CheckboxCardField: React.FC<CheckboxCardFieldProps> = ({
6768
}
6869

6970
return (
71+
// biome-ignore lint/a11y/useSemanticElements: <role used to concur to chakra standard>
7072
<HStack
7173
as="fieldset"
7274
id={id}
@@ -85,6 +87,7 @@ export const CheckboxCardField: React.FC<CheckboxCardFieldProps> = ({
8587
const isChecked = value.includes(opt.value)
8688
const disabled = opt.isDisabled || (!isChecked && isMaxed)
8789
return (
90+
// biome-ignore lint/a11y/useSemanticElements: <role used to concur to chakra standard>
8891
<Box
8992
key={opt.value}
9093
data-checkbox-card
@@ -114,7 +117,7 @@ export const CheckboxCardField: React.FC<CheckboxCardFieldProps> = ({
114117
}}
115118
>
116119
<Flex align="center" gap="2">
117-
{opt.icon ? <Icon as={opt.icon as any} boxSize="16px" /> : null}
120+
{opt.icon ? <Icon as={opt.icon as As} boxSize="16px" /> : null}
118121
<Text fontSize="sm" noOfLines={1}>
119122
{opt.label}
120123
</Text>

packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx

Lines changed: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
11
import {
22
Flex,
3+
Icon,
4+
IconButton,
35
Input,
6+
InputGroup,
7+
InputRightElement,
48
Popover,
59
PopoverBody,
610
PopoverContent,
711
PopoverTrigger,
812
} from "@chakra-ui/react"
13+
import { PiX } from "@react-icons/all-files/pi/PiX"
914
import { memo, useEffect, useState } from "react"
10-
import ColorPicker from "react-best-gradient-color-picker"
15+
import ColorPicker, {
16+
type ColorPickerProps,
17+
} from "react-best-gradient-color-picker"
1118
import { useDebounce } from "../../hooks/useDebounce"
1219

1320
const ColorPickerField = ({
1421
fieldName,
1522
value,
1623
setValue,
24+
fieldProps,
25+
isClearable,
1726
}: {
1827
fieldName: string
1928
value: string
2029
setValue: (name: string, value: string) => void
30+
fieldProps?: ColorPickerProps
31+
isClearable?: boolean
2132
}) => {
2233
const [localValue, setLocalValue] = useState<string>(value)
2334

35+
/**
36+
* @note: This parsing behavior is not a bug, it has been mimicked to match the downstream service
37+
* logic i.e. parseInt(hexAlpha, 10) / 100, which parses the hex digits as decimal, stopping at
38+
* non-digit characters.
39+
*/
40+
const parseAlphaLikeDownstream = (hexAlpha: string): number => {
41+
const parsed = parseInt(hexAlpha, 10)
42+
return Number.isNaN(parsed) ? 0 : parsed / 100
43+
}
44+
45+
/**
46+
* Helper function to convert alpha back to hex format that will parse correctly downstream.
47+
* We need to find a hex value that, when parsed as decimal by downstream, gives us the desired alpha.
48+
*
49+
* For example:
50+
* - If alpha is 0.99, we want downstream to get 99, so we need "99" in hex
51+
* - If alpha is 0.5, we want downstream to get 50, so we need "50" in hex
52+
*/
53+
const alphaToHexForDownstream = (alpha: number): string => {
54+
const targetDecimal = Math.round(alpha * 100)
55+
const clampedDecimal = Math.max(0, Math.min(99, targetDecimal))
56+
return clampedDecimal.toString().padStart(2, "0")
57+
}
58+
59+
// Convert a color from downstream format to standard format for the color picker
60+
const convertDownstreamToStandard = (color: string): string => {
61+
if (!color || !color?.startsWith("#") || color?.length !== 9) {
62+
return color
63+
}
64+
65+
const rgb = color.slice(1, 7)
66+
const alphaHex = color.slice(7, 9)
67+
const parsedAlpha = parseAlphaLikeDownstream(alphaHex)
68+
69+
// Convert to standard 0-255 range
70+
const standardAlphaInt = Math.round(parsedAlpha * 255)
71+
const standardAlphaHex = standardAlphaInt.toString(16).padStart(2, "0")
72+
73+
return `#${rgb}${standardAlphaHex}`
74+
}
75+
76+
// Get the preview color that shows what downstream will actually render
77+
const getPreviewColor = (color: string): string => {
78+
if (!color || !color?.startsWith("#")) {
79+
return color
80+
}
81+
82+
if (color.length === 9) {
83+
// Has alpha channel - convert using downstream logic
84+
return convertDownstreamToStandard(color)
85+
}
86+
87+
return color
88+
}
89+
2490
const handleColorChange = (color: string) => {
2591
const parts = color.match(/[\d.]+/g)?.map(Number) ?? []
2692

@@ -35,12 +101,12 @@ const ColorPickerField = ({
35101
.map((v) => v.toString(16).padStart(2, "0"))
36102
.join("")
37103

38-
if (a === undefined) {
104+
if (fieldProps?.hideOpacity === true || a === undefined) {
39105
setLocalValue(`#${rgbHex}`)
40106
} else {
41107
const alphaDec = a > 1 ? a / 100 : a
42-
const alphaInt = clamp8(Math.round(alphaDec * 255))
43-
setLocalValue(`#${rgbHex}${alphaInt.toString(16).padStart(2, "0")}`)
108+
const alphaHex = alphaToHexForDownstream(alphaDec)
109+
setLocalValue(`#${rgbHex}${alphaHex}`)
44110
}
45111
}
46112

@@ -50,27 +116,50 @@ const ColorPickerField = ({
50116
setValue(fieldName, debouncedValue)
51117
}, [debouncedValue, fieldName, setValue])
52118

119+
useEffect(() => {
120+
setLocalValue(value)
121+
}, [value])
122+
123+
const handleClear = () => {
124+
setLocalValue("")
125+
setValue(fieldName, "")
126+
}
127+
53128
return (
54129
<Flex direction="column" gap="2">
55130
<Flex>
56-
<Input
57-
size="md"
58-
value={localValue}
59-
onChange={(e) => {
60-
const newValue = e.target.value
61-
if (newValue.match(/^#[0-9A-Fa-f]{0,8}$/)) {
62-
setLocalValue(newValue)
63-
} else if (newValue === "") {
64-
setLocalValue("")
65-
}
66-
}}
67-
borderColor="gray.200"
68-
placeholder="#FFFFFF"
69-
fontFamily="mono"
70-
borderRadius="4px"
71-
borderRightRadius="0"
72-
width="calc(100% - var(--chakra-space-10))"
73-
/>
131+
<InputGroup width="calc(100% - var(--chakra-space-10))">
132+
<Input
133+
size="md"
134+
value={localValue}
135+
onChange={(e) => {
136+
const newValue = e.target.value
137+
if (newValue.match(/^#[0-9A-Fa-f]{0,8}$/)) {
138+
setLocalValue(newValue)
139+
} else if (newValue === "") {
140+
setLocalValue("")
141+
}
142+
}}
143+
borderColor="gray.200"
144+
placeholder="#FFFFFF"
145+
fontFamily="mono"
146+
borderRadius="4px"
147+
borderRightRadius="0"
148+
pr={isClearable && localValue ? "8" : undefined}
149+
/>
150+
{isClearable && localValue && (
151+
<InputRightElement>
152+
<IconButton
153+
aria-label="Clear color"
154+
icon={<Icon as={PiX} boxSize="3" />}
155+
size="xs"
156+
variant="ghost"
157+
onClick={handleClear}
158+
tabIndex={-1}
159+
/>
160+
</InputRightElement>
161+
)}
162+
</InputGroup>
74163
<Popover
75164
placement="auto"
76165
closeOnBlur={true}
@@ -84,7 +173,7 @@ const ColorPickerField = ({
84173
height="10"
85174
align="center"
86175
justify="center"
87-
bg={localValue}
176+
bg={getPreviewColor(localValue)}
88177
borderWidth="1px"
89178
borderColor="gray.200"
90179
borderLeft="0"
@@ -95,7 +184,7 @@ const ColorPickerField = ({
95184
<PopoverContent p="2" width="auto" zIndex={1400}>
96185
<PopoverBody p="0">
97186
<ColorPicker
98-
value={localValue}
187+
value={convertDownstreamToStandard(localValue)}
99188
onChange={handleColorChange}
100189
disableDarkMode
101190
hideGradientType
@@ -107,6 +196,8 @@ const ColorPickerField = ({
107196
hideInputs
108197
hideAdvancedSliders
109198
hideColorGuide
199+
// @ts-expect-error - fieldProps may include props not declared in ColorPickerProps, but they are intentionally forwarded
200+
{...fieldProps}
110201
/>
111202
</PopoverBody>
112203
</PopoverContent>

0 commit comments

Comments
 (0)