diff --git a/packages/imagekit-editor-dev/src/components/RetryableImage.tsx b/packages/imagekit-editor-dev/src/components/RetryableImage.tsx index 8b3b3ed..1f95585 100644 --- a/packages/imagekit-editor-dev/src/components/RetryableImage.tsx +++ b/packages/imagekit-editor-dev/src/components/RetryableImage.tsx @@ -11,7 +11,6 @@ import { } from "@chakra-ui/react" import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useVisibility } from "../hooks/useVisibility" -import { useEditorStore } from "../store" export interface RetryableImageProps extends ImageProps { maxRetries?: number @@ -105,11 +104,12 @@ export default function RetryableImage(props: RetryableImageProps) { setProbing(true) }, [currentSrcBase, src]) + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (!src) return if (lazy && !visible) return setAttempt(0) - beginLoad(0) + beginLoad() }, [src, visible, lazy]) const scheduleRetry = useCallback(() => { @@ -156,7 +156,11 @@ export default function RetryableImage(props: RetryableImageProps) { } return ( - + } + position="relative" + display="inline-block" + > {error ? (
= ({ } return ( + // biome-ignore lint/a11y/useSemanticElements: = ({ const isChecked = value.includes(opt.value) const disabled = opt.isDisabled || (!isChecked && isMaxed) return ( + // biome-ignore lint/a11y/useSemanticElements: = ({ }} > - {opt.icon ? : null} + {opt.icon ? : null} {opt.label} diff --git a/packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx b/packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx index 278de52..88419c4 100644 --- a/packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx +++ b/packages/imagekit-editor-dev/src/components/common/ColorPickerField.tsx @@ -7,7 +7,9 @@ import { PopoverTrigger, } from "@chakra-ui/react" import { memo, useEffect, useState } from "react" -import ColorPicker, { ColorPickerProps } from "react-best-gradient-color-picker" +import ColorPicker, { + type ColorPickerProps, +} from "react-best-gradient-color-picker" import { useDebounce } from "../../hooks/useDebounce" const ColorPickerField = ({ diff --git a/packages/imagekit-editor-dev/src/components/common/CornerRadiusInput.tsx b/packages/imagekit-editor-dev/src/components/common/CornerRadiusInput.tsx index 22b2c8e..dce5a0c 100644 --- a/packages/imagekit-editor-dev/src/components/common/CornerRadiusInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/CornerRadiusInput.tsx @@ -3,23 +3,22 @@ import { Flex, HStack, Icon, - Text, + IconButton, Input, InputGroup, InputLeftElement, - IconButton, - useColorModeValue, + Text, Tooltip, + useColorModeValue, } from "@chakra-ui/react" -import { set } from "lodash" -import type * as React from "react" -import { useState, useEffect, forwardRef } from "react" +import { RxCornerBottomLeft } from "@react-icons/all-files/rx/RxCornerBottomLeft" +import { RxCornerBottomRight } from "@react-icons/all-files/rx/RxCornerBottomRight" import { RxCornerTopLeft } from "@react-icons/all-files/rx/RxCornerTopLeft" import { RxCornerTopRight } from "@react-icons/all-files/rx/RxCornerTopRight" -import { RxCornerBottomRight } from "@react-icons/all-files/rx/RxCornerBottomRight" -import { RxCornerBottomLeft } from "@react-icons/all-files/rx/RxCornerBottomLeft" import { TbBorderCorners } from "@react-icons/all-files/tb/TbBorderCorners" -import { FieldErrors } from "react-hook-form" +import { set } from "lodash" +import type * as React from "react" +import { useEffect, useState } from "react" type RadiusMode = "uniform" | "individual" @@ -28,14 +27,6 @@ export type RadiusState = { radius: RadiusObject | string } -type RadiusInputFieldProps = { - id?: string - onChange: (value: RadiusState) => void - errors?: FieldErrors> - name: string, - value?: Partial -} - export type RadiusObject = { topLeft: string | "max" topRight: string | "max" @@ -43,13 +34,36 @@ export type RadiusObject = { bottomLeft: string | "max" } +type ErrorObject = { + message: string +} + +type CornerErrors = { + [key in keyof RadiusObject]?: ErrorObject +} & ErrorObject + +export type RadiusErrors = Record< + string, + { + radius?: CornerErrors + } +> + +type RadiusInputFieldProps = { + id?: string + onChange: (value: RadiusState) => void + errors?: RadiusErrors + name: string + value?: Partial +} + type RadiusDirection = "topLeft" | "topRight" | "bottomRight" | "bottomLeft" function getUpdatedRadiusValue( current: RadiusObject | string, corner: RadiusDirection | "all", value: string, - mode: "uniform" | "individual" + mode: "uniform" | "individual", ): RadiusObject | string { let inputValue: RadiusObject | number | string try { @@ -60,14 +74,21 @@ function getUpdatedRadiusValue( if (mode === "uniform") { if (inputValue === "") { return "" - } else if (typeof inputValue === "string" || typeof inputValue === "number") { + } else if ( + typeof inputValue === "string" || + typeof inputValue === "number" + ) { return inputValue.toString() } else { const { topLeft, topRight, bottomRight, bottomLeft } = inputValue - if (topLeft === topRight && topLeft === bottomRight && topLeft === bottomLeft) { + if ( + topLeft === topRight && + topLeft === bottomRight && + topLeft === bottomLeft + ) { return topLeft } else { - return ""; + return "" } } } else { @@ -75,9 +96,15 @@ function getUpdatedRadiusValue( if (typeof inputValue === "string" || typeof inputValue === "number") { commonValue = inputValue.toString() } - const updatedRadius = current && typeof current === "object" - ? { ...current } - : { topLeft: commonValue, topRight: commonValue, bottomRight: commonValue, bottomLeft: commonValue } + const updatedRadius = + current && typeof current === "object" + ? { ...current } + : { + topLeft: commonValue, + topRight: commonValue, + bottomRight: commonValue, + bottomLeft: commonValue, + } if (corner !== "all") { set(updatedRadius, corner, inputValue.toString()) } @@ -90,29 +117,36 @@ export const RadiusInputField: React.FC = ({ onChange, errors, name: propertyName, - value + value, }) => { - const [radiusMode, setRadiusMode] = useState(value?.mode ?? "uniform") - const [radiusValue, setRadiusValue] = useState(value?.radius ?? "") + const [radiusMode, setRadiusMode] = useState( + value?.mode ?? "uniform", + ) + const [radiusValue, setRadiusValue] = useState( + value?.radius ?? "", + ) const errorRed = useColorModeValue("red.500", "red.300") const activeColor = useColorModeValue("blue.500", "blue.600") const inactiveColor = useColorModeValue("gray.600", "gray.400") + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - const formatRadiusValue = (value: RadiusObject | string): string | RadiusObject => { + const formatRadiusValue = ( + value: RadiusObject | string, + ): string | RadiusObject => { if (value === "") return "" if (typeof value === "string") { return value } else { - return value; + return value } } const formattedValue = formatRadiusValue(radiusValue) onChange({ mode: radiusMode, radius: formattedValue }) }, [radiusValue, radiusMode]) - return ( + // biome-ignore lint/a11y/useSemanticElements: = ({ { const val = e.target.value - setRadiusValue(getUpdatedRadiusValue( - radiusValue, - "all", - val, - radiusMode - )) + setRadiusValue( + getUpdatedRadiusValue(radiusValue, "all", val, radiusMode), + ) }} value={typeof radiusValue === "string" ? radiusValue : ""} placeholder="Uniform Radius" isInvalid={!!errors?.[propertyName]?.radius} fontSize="sm" /> - {errors?.[propertyName]?.radius?.message} + + {errors?.[propertyName]?.radius?.message} + ) : ( + // biome-ignore lint/complexity/noUselessFragments: <> {[ { name: "topLeft", label: "Top Left", icon: RxCornerTopLeft }, { name: "topRight", label: "Top Right", icon: RxCornerTopRight }, - { name: "bottomLeft", label: "Bottom Left", icon: RxCornerBottomLeft }, - { name: "bottomRight", label: "Bottom Right", icon: RxCornerBottomRight }, + { + name: "bottomLeft", + label: "Bottom Left", + icon: RxCornerBottomLeft, + }, + { + name: "bottomRight", + label: "Bottom Right", + icon: RxCornerBottomRight, + }, ].map(({ name, label, icon }) => ( @@ -157,20 +199,35 @@ export const RadiusInputField: React.FC = ({ { const val = e.target.value - setRadiusValue(getUpdatedRadiusValue( - radiusValue, - name as RadiusDirection, - val, - radiusMode - )) + setRadiusValue( + getUpdatedRadiusValue( + radiusValue, + name as RadiusDirection, + val, + radiusMode, + ), + ) }} - value={typeof radiusValue === "object" ? radiusValue?.[name as RadiusDirection] ?? "" : ""} + value={ + typeof radiusValue === "object" + ? (radiusValue?.[name as RadiusDirection] ?? "") + : "" + } placeholder={label} - isInvalid={!!errors?.[propertyName]?.radius?.[name as RadiusDirection]} + isInvalid={ + !!errors?.[propertyName]?.radius?.[ + name as RadiusDirection + ] + } fontSize="sm" /> - {errors?.[propertyName]?.radius?.[name as RadiusDirection]?.message} + + { + errors?.[propertyName]?.radius?.[name as RadiusDirection] + ?.message + } + ))} @@ -178,32 +235,43 @@ export const RadiusInputField: React.FC = ({ } padding="0.05em" onClick={() => { - const newRadiusMode = radiusMode === "uniform" ? "individual" : "uniform" - setRadiusValue(getUpdatedRadiusValue( - radiusValue, - "all", - JSON.stringify(radiusValue), - newRadiusMode - )) + const newRadiusMode = + radiusMode === "uniform" ? "individual" : "uniform" + setRadiusValue( + getUpdatedRadiusValue( + radiusValue, + "all", + JSON.stringify(radiusValue), + newRadiusMode, + ), + ) setRadiusMode(newRadiusMode) }} variant="outline" diff --git a/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx b/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx index 6ae4210..d6594f4 100644 --- a/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/DistortPerspectiveInput.tsx @@ -1,43 +1,50 @@ import { Box, - Flex, + FormLabel, HStack, - VStack, Icon, - Text, Input, InputGroup, - InputLeftElement, - IconButton, + InputLeftAddon, + Text, useColorModeValue, - Tooltip, -} from "@chakra-ui/react"; -import type * as React from "react"; -import { useState, useEffect } from "react"; -import { RxArrowTopLeft } from "@react-icons/all-files/rx/RxArrowTopLeft"; -import { RxArrowTopRight } from "@react-icons/all-files/rx/RxArrowTopRight"; -import { RxArrowBottomRight } from "@react-icons/all-files/rx/RxArrowBottomRight"; -import { RxArrowBottomLeft } from "@react-icons/all-files/rx/RxArrowBottomLeft"; -import { FieldErrors } from "react-hook-form"; - -type DistorPerspectiveFieldProps = { - name: string; - id?: string; - onChange: (value: PerspectiveObject) => void; - errors?: FieldErrors>; - value?: PerspectiveObject; -}; + VStack, +} from "@chakra-ui/react" +import { RxArrowBottomLeft } from "@react-icons/all-files/rx/RxArrowBottomLeft" +import { RxArrowBottomRight } from "@react-icons/all-files/rx/RxArrowBottomRight" +import { RxArrowTopLeft } from "@react-icons/all-files/rx/RxArrowTopLeft" +import { RxArrowTopRight } from "@react-icons/all-files/rx/RxArrowTopRight" +import type * as React from "react" +import { useEffect, useState } from "react" export type PerspectiveObject = { - x1: string; - y1: string; - x2: string; - y2: string; - x3: string; - y3: string; - x4: string; - y4: string; -}; + x1: string + y1: string + x2: string + y2: string + x3: string + y3: string + x4: string + y4: string +} + +type ErrorObject = { + message: string +} + +type CornerErrors = { + [key in keyof PerspectiveObject]?: ErrorObject +} & ErrorObject + +export type PerspectiveErrors = Record + +type DistorPerspectiveFieldProps = { + name: string + id?: string + onChange: (value: PerspectiveObject) => void + errors?: PerspectiveErrors + value?: PerspectiveObject +} export const DistortPerspectiveInput: React.FC = ({ id, @@ -46,200 +53,132 @@ export const DistortPerspectiveInput: React.FC = ({ name: propertyName, value, }) => { - const [perspective, setPerspective] = useState(value ?? { - x1: "", - y1: "", - x2: "", - y2: "", - x3: "", - y3: "", - x4: "", - y4: "", - }); - const errorRed = useColorModeValue("red.500", "red.300"); - const leftAccessoryBackground = useColorModeValue("gray.100", "gray.700"); + const [perspective, setPerspective] = useState( + value ?? { + x1: "", + y1: "", + x2: "", + y2: "", + x3: "", + y3: "", + x4: "", + y4: "", + }, + ) + const errorRed = useColorModeValue("red.500", "red.300") + const leftAccessoryBackground = useColorModeValue("gray.100", "gray.700") function handleFieldChange(fieldName: string) { return (e: React.ChangeEvent) => { - const val = e.target.value.trim(); + const val = e.target.value.trim() setPerspective((prev) => ({ ...prev, - [fieldName]: val, - })); - }; + [fieldName]: val?.toUpperCase(), + })) + } } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - onChange(perspective); - }, [perspective]); + onChange(perspective) + }, [perspective]) return ( - - - - - - - - - - - {[ - errors?.[propertyName]?.x1?.message, - errors?.[propertyName]?.y1?.message, - ] - .filter(Boolean) - .join(". ")} - - - - - - - - - - - - - {[ - errors?.[propertyName]?.x2?.message, - errors?.[propertyName]?.y2?.message, - ] - .filter(Boolean) - .join(". ")} - - - - - - - - - - - - - {[ - errors?.[propertyName]?.x3?.message, - errors?.[propertyName]?.y3?.message, - ] - .filter(Boolean) - .join(". ")} - - + // biome-ignore lint/a11y/useSemanticElements: + + {[ + { + label: "Top left", + name: "topLeft", + icon: RxArrowTopLeft, + x: "x1", + y: "y1", + }, + { + label: "Top right", + name: "topRight", + icon: RxArrowTopRight, + x: "x2", + y: "y2", + }, + { + label: "Bottom right", + name: "bottomRight", + icon: RxArrowBottomRight, + x: "x3", + y: "y3", + }, + { + label: "Bottom left", + name: "bottomLeft", + icon: RxArrowBottomLeft, + x: "x4", + y: "y4", + }, + ].map(({ label, name, icon, x, y }) => ( + + + + + + + {label} corner coordinates + + + + + + {x.toUpperCase()} + + + + { + errors?.[propertyName]?.[x as keyof PerspectiveObject] + ?.message + } + + - - - - - - - - - - {[ - errors?.[propertyName]?.x4?.message, - errors?.[propertyName]?.y4?.message, - ] - .filter(Boolean) - .join(". ")} - - + + + {y.toUpperCase()} + + + + { + errors?.[propertyName]?.[y as keyof PerspectiveObject] + ?.message + } + + + + + ))} - ); -}; + ) +} -export default DistortPerspectiveInput; +export default DistortPerspectiveInput diff --git a/packages/imagekit-editor-dev/src/components/common/GradientPicker.tsx b/packages/imagekit-editor-dev/src/components/common/GradientPicker.tsx index 8cfb545..aa4be82 100644 --- a/packages/imagekit-editor-dev/src/components/common/GradientPicker.tsx +++ b/packages/imagekit-editor-dev/src/components/common/GradientPicker.tsx @@ -1,56 +1,56 @@ import { + Box, Flex, + FormLabel, Input, Popover, PopoverBody, PopoverContent, PopoverTrigger, - FormLabel, - Box, Text, useColorModeValue, -} from "@chakra-ui/react"; -import { memo, useEffect, useState, useMemo } from "react"; -import ColorPicker, { useColorPicker } from "react-best-gradient-color-picker"; -import { useDebounce } from "../../hooks/useDebounce"; -import AnchorField from "./AnchorField"; -import RadioCardField from "./RadioCardField"; -import { TbAngle } from "@react-icons/all-files/tb/TbAngle"; -import { BsArrowsMove } from "@react-icons/all-files/bs/BsArrowsMove"; -import { FieldErrors } from "react-hook-form"; +} from "@chakra-ui/react" +import { BsArrowsMove } from "@react-icons/all-files/bs/BsArrowsMove" +import { TbAngle } from "@react-icons/all-files/tb/TbAngle" +import { memo, useEffect, useState } from "react" +import ColorPicker, { useColorPicker } from "react-best-gradient-color-picker" +import type { FieldErrors } from "react-hook-form" +import { useDebounce } from "../../hooks/useDebounce" +import AnchorField from "./AnchorField" +import RadioCardField from "./RadioCardField" export type GradientPickerState = { - from: string; - to: string; - direction: number | string; - stopPoint: number | string; -}; + from: string + to: string + direction: number | string + stopPoint: number | string +} -type DirectionMode = "direction" | "degrees"; +type DirectionMode = "direction" | "degrees" function rgbaToHex(rgba: string): string { - const parts = rgba.match(/[\d.]+/g)?.map(Number) ?? []; + const parts = rgba.match(/[\d.]+/g)?.map(Number) ?? [] - if (parts.length < 3) return "#000000"; + if (parts.length < 3) return "#000000" - const [r, g, b, a] = parts; + const [r, g, b, a] = parts - const clamp8 = (v: number) => Math.max(0, Math.min(255, v)); + const clamp8 = (v: number) => Math.max(0, Math.min(255, v)) const rgbHex = [r, g, b] .map(clamp8) .map((v) => v.toString(16).padStart(2, "0")) - .join(""); + .join("") if (a === undefined) { - return `#${rgbHex}`; + return `#${rgbHex}` } - const alphaDec = a > 1 ? a / 100 : a; + const alphaDec = a > 1 ? a / 100 : a const alphaHex = Math.round(alphaDec * 255) .toString(16) .padStart(2, "0") - .toUpperCase(); - return `#${rgbHex}${alphaHex}`; + .toUpperCase() + return `#${rgbHex}${alphaHex}` } const GradientPickerField = ({ @@ -59,24 +59,24 @@ const GradientPickerField = ({ value, errors, }: { - fieldName: string; - setValue: (name: string, value: GradientPickerState | string) => void; - value?: GradientPickerState | null; - errors?: FieldErrors>; + fieldName: string + setValue: (name: string, value: GradientPickerState | string) => void + value?: GradientPickerState | null + errors?: FieldErrors> }) => { function getLinearGradientString(value: GradientPickerState): string { - let direction = ""; - const dirInt = Number(value.direction as string); - if (!isNaN(dirInt)) { - direction = `${dirInt}deg`; + let direction = "" + const dirInt = Number(value.direction as string) + if (!Number.isNaN(dirInt)) { + direction = `${dirInt}deg` } else { - direction = `to ${String(value.direction).split("_").join(" ")}`; + direction = `to ${String(value.direction).split("_").join(" ")}` } const stopPoint = typeof value.stopPoint === "number" ? value.stopPoint - : Number(value.stopPoint); - return `linear-gradient(${direction}, ${value.from} 0%, ${value.to} ${stopPoint}%)`; + : Number(value.stopPoint) + return `linear-gradient(${direction}, ${value.from} 0%, ${value.to} ${stopPoint}%)` } const [localValue, setLocalValue] = useState( @@ -86,22 +86,21 @@ const GradientPickerField = ({ direction: "bottom", stopPoint: 100, }, - ); - const [directionMode, setDirectionMode] = - useState("direction"); + ) + const [directionMode, setDirectionMode] = useState("direction") const [gradient, setGradient] = useState( getLinearGradientString(localValue), - ); + ) - const { getGradientObject } = useColorPicker(gradient, setGradient); + const { getGradientObject } = useColorPicker(gradient, setGradient) function getAngleValue(): number | string { - const dirInt = Number(localValue.direction as string); - if (!isNaN(dirInt)) { - return dirInt || ""; + const dirInt = Number(localValue.direction as string) + if (!Number.isNaN(dirInt)) { + return dirInt || "" } - const direction = localValue.direction as string; + const direction = localValue.direction as string const directionMap: Record = { top: 0, top_right: 45, @@ -111,16 +110,16 @@ const GradientPickerField = ({ bottom_left: 225, left: 270, top_left: 315, - }; - return directionMap[direction] || ""; + } + return directionMap[direction] || "" } function getDirectionValue(): string { - const dirInt = Number(localValue.direction as string); - if (isNaN(dirInt)) { - return String(localValue.direction); + const dirInt = Number(localValue.direction as string) + if (Number.isNaN(dirInt)) { + return String(localValue.direction) } - const nearestAngle = Math.round(dirInt / 45) * 45; + const nearestAngle = Math.round(dirInt / 45) * 45 const angleMap: Record = { 0: "top", 45: "top_right", @@ -130,31 +129,32 @@ const GradientPickerField = ({ 225: "bottom_left", 270: "left", 315: "top_left", - }; - return angleMap[nearestAngle] || "bottom"; + } + return angleMap[nearestAngle] || "bottom" } - const debouncedValue = useDebounce(localValue, 500); + const debouncedValue = useDebounce(localValue, 500) function handleGradientChange(gradientVal: string) { - const cleanedGradient = gradientVal.replace(/NaNdeg\s*,/, ""); - let gradientObj; + const cleanedGradient = gradientVal.replace(/NaNdeg\s*,/, "") + let gradientObj: ReturnType try { - gradientObj = getGradientObject(cleanedGradient); - } catch (error) { - return; + gradientObj = getGradientObject(cleanedGradient) + } catch (e) { + console.error("Failed to parse gradient:", e) + return } - if (!gradientObj || !gradientObj.isGradient) return; + if (!gradientObj || !gradientObj.isGradient) return - const { colors } = gradientObj; - if (colors.length !== 2) return; - if (colors[0].left !== 0) return; - setGradient(cleanedGradient); + const { colors } = gradientObj + if (colors.length !== 2) return + if (colors[0].left !== 0) return + setGradient(cleanedGradient) - const fromColor = rgbaToHex(colors[0].value).toUpperCase(); - const toColor = rgbaToHex(colors[1].value).toUpperCase(); - const stopPoint = colors[1].left; + const fromColor = rgbaToHex(colors[0].value).toUpperCase() + const toColor = rgbaToHex(colors[1].value).toUpperCase() + const stopPoint = colors[1].left if ( fromColor !== localValue.from || @@ -166,21 +166,21 @@ const GradientPickerField = ({ from: fromColor, to: toColor, stopPoint: stopPoint, - }); + }) } } function applyGradientInputChanges(newValue: GradientPickerState) { - const gradientString = getLinearGradientString(newValue); - setGradient(gradientString); - setLocalValue(newValue); + const gradientString = getLinearGradientString(newValue) + setGradient(gradientString) + setLocalValue(newValue) } useEffect(() => { - setValue(fieldName, debouncedValue); - }, [debouncedValue, fieldName, setValue]); + setValue(fieldName, debouncedValue) + }, [debouncedValue, fieldName, setValue]) - const errorRed = useColorModeValue("red.500", "red.300"); + const errorRed = useColorModeValue("red.500", "red.300") return ( @@ -229,11 +229,11 @@ const GradientPickerField = ({ size="md" value={localValue.from} onChange={(e) => { - const newValue = e.target.value; + const newValue = e.target.value if (newValue.match(/^#[0-9A-Fa-f]{0,8}$/)) { - applyGradientInputChanges({ ...localValue, from: newValue }); + applyGradientInputChanges({ ...localValue, from: newValue }) } else if (newValue === "") { - applyGradientInputChanges({ ...localValue, from: "" }); + applyGradientInputChanges({ ...localValue, from: "" }) } }} borderColor="gray.200" @@ -254,11 +254,11 @@ const GradientPickerField = ({ size="md" value={localValue.to} onChange={(e) => { - const newValue = e.target.value; + const newValue = e.target.value if (newValue.match(/^#[0-9A-Fa-f]{0,8}$/)) { - applyGradientInputChanges({ ...localValue, to: newValue }); + applyGradientInputChanges({ ...localValue, to: newValue }) } else if (newValue === "") { - applyGradientInputChanges({ ...localValue, to: "" }); + applyGradientInputChanges({ ...localValue, to: "" }) } }} borderColor="gray.200" @@ -283,13 +283,13 @@ const GradientPickerField = ({ ]} value={directionMode} onChange={(val) => { - setDirectionMode((val || "direction") as DirectionMode); + setDirectionMode((val || "direction") as DirectionMode) const newDirection = - val === "direction" ? getDirectionValue() : getAngleValue(); + val === "direction" ? getDirectionValue() : getAngleValue() applyGradientInputChanges({ ...localValue, direction: newDirection, - }); + }) }} /> @@ -297,7 +297,7 @@ const GradientPickerField = ({ { - applyGradientInputChanges({ ...localValue, direction: val }); + applyGradientInputChanges({ ...localValue, direction: val }) }} positions={[ "top", @@ -318,14 +318,14 @@ const GradientPickerField = ({ min={0} max={359} onChange={(e) => { - const newValue = e.target.value.trim(); + const newValue = e.target.value.trim() if (newValue === "") { - applyGradientInputChanges({ ...localValue, direction: "" }); - return; + applyGradientInputChanges({ ...localValue, direction: "" }) + return } - const intVal = Number(newValue); - if (intVal < 0 || intVal > 359) return; - applyGradientInputChanges({ ...localValue, direction: intVal }); + const intVal = Number(newValue) + if (intVal < 0 || intVal > 359) return + applyGradientInputChanges({ ...localValue, direction: intVal }) }} borderColor="gray.200" placeholder="0" @@ -348,17 +348,17 @@ const GradientPickerField = ({ min={1} max={100} onChange={(e) => { - const newValue = e.target.value.trim(); + const newValue = e.target.value.trim() if (newValue === "") { - applyGradientInputChanges({ ...localValue, stopPoint: "" }); - return; + applyGradientInputChanges({ ...localValue, stopPoint: "" }) + return } - const intVal = Number(newValue); - if (intVal < 1 || intVal > 100) return; + const intVal = Number(newValue) + if (intVal < 1 || intVal > 100) return applyGradientInputChanges({ ...localValue, stopPoint: intVal, - }); + }) }} borderColor="gray.200" placeholder="100" @@ -369,7 +369,7 @@ const GradientPickerField = ({ - ); -}; + ) +} -export default memo(GradientPickerField); +export default memo(GradientPickerField) diff --git a/packages/imagekit-editor-dev/src/components/common/Hover.tsx b/packages/imagekit-editor-dev/src/components/common/Hover.tsx index 09f7929..f7fbc6e 100644 --- a/packages/imagekit-editor-dev/src/components/common/Hover.tsx +++ b/packages/imagekit-editor-dev/src/components/common/Hover.tsx @@ -1,5 +1,5 @@ import { Box, type BoxProps, Flex, type FlexProps } from "@chakra-ui/react" -import { useState, useEffect, useRef, useCallback } from "react" +import { useCallback, useEffect, useRef, useState } from "react" interface FlexHoverProps extends FlexProps { children(isHover: boolean): JSX.Element @@ -20,29 +20,29 @@ const Hover = ({ const handleClickOutside = useCallback((event: MouseEvent): void => { const hoverArea = hoverAreaRef.current - if ( - hoverArea && - !hoverArea.contains(event.target as Node) - ) { + if (hoverArea && !hoverArea.contains(event.target as Node)) { setIsHover(false) } }, []) - const debouncedHandleClickOutside = useCallback((event: MouseEvent): void => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current) - } - debounceTimerRef.current = setTimeout(() => { - handleClickOutside(event) - }, 100) - }, [handleClickOutside]) + const debouncedHandleClickOutside = useCallback( + (event: MouseEvent): void => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current) + } + debounceTimerRef.current = setTimeout(() => { + handleClickOutside(event) + }, 100) + }, + [handleClickOutside], + ) useEffect(() => { - document.addEventListener('mousedown', handleClickOutside) - document.addEventListener('mouseover', debouncedHandleClickOutside) + document.addEventListener("mousedown", handleClickOutside) + document.addEventListener("mouseover", debouncedHandleClickOutside) return () => { - document.removeEventListener('mousedown', handleClickOutside) - document.removeEventListener('mouseover', debouncedHandleClickOutside) + document.removeEventListener("mousedown", handleClickOutside) + document.removeEventListener("mouseover", debouncedHandleClickOutside) if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } diff --git a/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx b/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx index 58ad486..b2a8470 100644 --- a/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx @@ -3,23 +3,22 @@ import { Flex, HStack, Icon, - Text, + IconButton, Input, InputGroup, InputLeftElement, - IconButton, - useColorModeValue, + Text, Tooltip, + useColorModeValue, } from "@chakra-ui/react" -import { set } from "lodash" -import type * as React from "react" -import { useState, useEffect, forwardRef } from "react" +import { LuArrowDownToLine } from "@react-icons/all-files/lu/LuArrowDownToLine" import { LuArrowLeftToLine } from "@react-icons/all-files/lu/LuArrowLeftToLine" import { LuArrowRightToLine } from "@react-icons/all-files/lu/LuArrowRightToLine" import { LuArrowUpToLine } from "@react-icons/all-files/lu/LuArrowUpToLine" -import { LuArrowDownToLine } from "@react-icons/all-files/lu/LuArrowDownToLine" import { TbBoxPadding } from "@react-icons/all-files/tb/TbBoxPadding" -import { FieldErrors } from "react-hook-form" +import { set } from "lodash" +import type * as React from "react" +import { useEffect, useState } from "react" type PaddingMode = "uniform" | "individual" @@ -30,14 +29,6 @@ export type PaddingState = { padding: number | PaddingObject | null | string } -type PaddingInputFieldProps = { - id?: string - onChange: (value: PaddingState) => void - errors?: FieldErrors> - name: string, - value?: Partial -} - export type PaddingObject = { top: number | null right: number | null @@ -45,11 +36,34 @@ export type PaddingObject = { left: number | null } +type ErrorObject = { + message: string +} + +type SidesErrors = { + [key in keyof PaddingObject]?: ErrorObject +} & ErrorObject + +export type PaddingErrors = Record< + string, + { + padding?: SidesErrors + } +> + +type PaddingInputFieldProps = { + id?: string + onChange: (value: PaddingState) => void + errors?: PaddingErrors + name: string + value?: Partial +} + function getUpdatedPaddingValue( current: number | PaddingObject | null | string, side: PaddingDirection | "all", value: string, - mode: "uniform" | "individual" + mode: "uniform" | "individual", ): number | PaddingObject | null | string { let inputValue: number | PaddingObject | null | string try { @@ -77,9 +91,15 @@ function getUpdatedPaddingValue( if (typeof inputValue === "number") { commonValue = inputValue } - const updatedPadding = current && typeof current === "object" - ? { ...current } - : { top: commonValue, right: commonValue, bottom: commonValue, left: commonValue } + const updatedPadding = + current && typeof current === "object" + ? { ...current } + : { + top: commonValue, + right: commonValue, + bottom: commonValue, + left: commonValue, + } if (side !== "all") { set(updatedPadding, side, inputValue) } @@ -92,31 +112,38 @@ export const PaddingInputField: React.FC = ({ onChange, errors, name: propertyName, - value + value, }) => { - const [paddingMode, setPaddingMode] = useState(value?.mode ?? "uniform") - const [paddingValue, setPaddingValue] = useState(value?.padding ?? "") + const [paddingMode, setPaddingMode] = useState( + value?.mode ?? "uniform", + ) + const [paddingValue, setPaddingValue] = useState< + number | PaddingObject | null | string + >(value?.padding ?? "") const errorRed = useColorModeValue("red.500", "red.300") const activeColor = useColorModeValue("blue.500", "blue.600") const inactiveColor = useColorModeValue("gray.600", "gray.400") + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - const formatPaddingValue = (value: number | PaddingObject | null | string): string | PaddingObject => { + const formatPaddingValue = ( + value: number | PaddingObject | null | string, + ): string | PaddingObject => { if (value === null) return "" if (typeof value === "number") { return value.toString() } else if (typeof value === "string") { return value } else { - return value; + return value } } const formattedValue = formatPaddingValue(paddingValue) onChange({ mode: paddingMode, padding: formattedValue }) }, [paddingValue, paddingMode]) - return ( + // biome-ignore lint/a11y/useSemanticElements: = ({ min={0} onChange={(e) => { const val = e.target.value - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - "all", - val, - paddingMode - )) + setPaddingValue( + getUpdatedPaddingValue(paddingValue, "all", val, paddingMode), + ) }} - value={["number", "string"].includes(typeof paddingValue) ? paddingValue : ""} + value={ + ["number", "string"].includes(typeof paddingValue) + ? (paddingValue as string | number) + : "" + } placeholder="Uniform Padding" isInvalid={!!errors?.[propertyName]?.padding} fontSize="sm" /> - {errors?.[propertyName]?.padding?.message} + + {errors?.[propertyName]?.padding?.message} + ) : ( + // biome-ignore lint/complexity/noUselessFragments: <> {[ { name: "top", label: "Top", icon: LuArrowUpToLine }, @@ -155,7 +186,7 @@ export const PaddingInputField: React.FC = ({ { name: "bottom", label: "Bottom", icon: LuArrowDownToLine }, { name: "left", label: "Left", icon: LuArrowLeftToLine }, ].map(({ name, label, icon }) => ( - + @@ -165,54 +196,79 @@ export const PaddingInputField: React.FC = ({ min={0} onChange={(e) => { const val = e.target.value - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - name as PaddingDirection, - val, - paddingMode - )) + setPaddingValue( + getUpdatedPaddingValue( + paddingValue, + name as PaddingDirection, + val, + paddingMode, + ), + ) }} - value={typeof paddingValue === "object" ? paddingValue?.[name as PaddingDirection] ?? "" : ""} + value={ + typeof paddingValue === "object" + ? (paddingValue?.[name as PaddingDirection] ?? "") + : "" + } placeholder={label} - isInvalid={!!errors?.[propertyName]?.padding?.[name as PaddingDirection]} + isInvalid={ + !!errors?.[propertyName]?.padding?.[ + name as PaddingDirection + ] + } fontSize="sm" /> - {errors?.[propertyName]?.padding?.[name as PaddingDirection]?.message} + + { + errors?.[propertyName]?.padding?.[name as PaddingDirection] + ?.message + } + - )) - } + ))} )} } padding="0.05em" onClick={() => { - const newPaddingMode = paddingMode === "uniform" ? "individual" : "uniform" - setPaddingValue(getUpdatedPaddingValue( - paddingValue, - "all", - JSON.stringify(paddingValue), - newPaddingMode - )) + const newPaddingMode = + paddingMode === "uniform" ? "individual" : "uniform" + setPaddingValue( + getUpdatedPaddingValue( + paddingValue, + "all", + JSON.stringify(paddingValue), + newPaddingMode, + ), + ) setPaddingMode(newPaddingMode) }} variant="outline" diff --git a/packages/imagekit-editor-dev/src/components/common/RadioCardField.tsx b/packages/imagekit-editor-dev/src/components/common/RadioCardField.tsx index 7f0d82e..3cdb7c8 100644 --- a/packages/imagekit-editor-dev/src/components/common/RadioCardField.tsx +++ b/packages/imagekit-editor-dev/src/components/common/RadioCardField.tsx @@ -58,6 +58,7 @@ export const RadioCardField: React.FC = ({ {options.map((opt) => { const isSelected = value === opt.value return ( + // biome-ignore lint/a11y/useSemanticElements: = ({ }} > - {opt.icon ? : null} + {opt.icon ? : null} {opt.label} diff --git a/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx b/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx index 3738483..4cc77b8 100644 --- a/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx +++ b/packages/imagekit-editor-dev/src/components/common/ZoomInput.tsx @@ -1,17 +1,16 @@ import { + ButtonGroup, HStack, + IconButton, Input, InputGroup, InputRightElement, - IconButton, - ButtonGroup, Text, - useColorModeValue, } from "@chakra-ui/react" -import type * as React from "react" -import { useState, useEffect } from "react" -import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus" import { AiOutlineMinus } from "@react-icons/all-files/ai/AiOutlineMinus" +import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus" +import type * as React from "react" +import { useEffect, useState } from "react" type ZoomInputFieldProps = { id?: string @@ -22,13 +21,12 @@ type ZoomInputFieldProps = { const STEP_SIZE = 10 - /** * Calculate the next zoom value when zooming in * Rounds up to the next step value */ function calculateZoomIn(currentValue: number): number { - return (Math.floor(currentValue / STEP_SIZE) * STEP_SIZE) + STEP_SIZE + return Math.floor(currentValue / STEP_SIZE) * STEP_SIZE + STEP_SIZE } /** @@ -36,7 +34,7 @@ function calculateZoomIn(currentValue: number): number { * Rounds down to the previous step value */ function calculateZoomOut(currentValue: number): number { - return (Math.ceil(currentValue / STEP_SIZE) * STEP_SIZE) - STEP_SIZE + return Math.ceil(currentValue / STEP_SIZE) * STEP_SIZE - STEP_SIZE } export const ZoomInput: React.FC = ({ @@ -45,9 +43,14 @@ export const ZoomInput: React.FC = ({ defaultValue = 100, value, }) => { - const [zoomValue, setZoomValue] = useState(value ?? defaultValue) - const [inputValue, setInputValue] = useState((value ?? defaultValue).toString()) + const [zoomValue, setZoomValue] = useState( + value ?? (defaultValue as number), + ) + const [inputValue, setInputValue] = useState( + (value ?? (defaultValue as number)).toString(), + ) + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { onChange(zoomValue) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -56,9 +59,9 @@ export const ZoomInput: React.FC = ({ const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value setInputValue(value) - + const numValue = Number(value) - if (!isNaN(numValue) && numValue >= 0) { + if (!Number.isNaN(numValue) && numValue >= 0) { setZoomValue(numValue) } } @@ -87,13 +90,8 @@ export const ZoomInput: React.FC = ({ } return ( - + // biome-ignore lint/a11y/useSemanticElements: + = ({ onClick={handleZoomIn} /> - ) } diff --git a/packages/imagekit-editor-dev/src/components/editor/GridView.tsx b/packages/imagekit-editor-dev/src/components/editor/GridView.tsx index 2a8bc9b..7671b8e 100644 --- a/packages/imagekit-editor-dev/src/components/editor/GridView.tsx +++ b/packages/imagekit-editor-dev/src/components/editor/GridView.tsx @@ -159,6 +159,7 @@ export const GridView: FC = ({ imageSize, onAddImage }) => { } isLoading={isSigning} onLoad={(event) => { + // biome-ignore lint/style/noNonNullAssertion: setImageDimensions(originalImageList[index]!.url, { width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight, diff --git a/packages/imagekit-editor-dev/src/components/editor/ListView.tsx b/packages/imagekit-editor-dev/src/components/editor/ListView.tsx index dac8b90..57657ff 100644 --- a/packages/imagekit-editor-dev/src/components/editor/ListView.tsx +++ b/packages/imagekit-editor-dev/src/components/editor/ListView.tsx @@ -59,6 +59,7 @@ export const ListView: FC = ({ onAddImage }) => { if (!currentImage) return const idx = imageList.findIndex((img) => img === currentImage) if (idx === -1) return + // biome-ignore lint/style/noNonNullAssertion: setImageDimensions(originalImageList[idx]!.url, { width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight, diff --git a/packages/imagekit-editor-dev/src/components/header/index.tsx b/packages/imagekit-editor-dev/src/components/header/index.tsx index 9e4cb91..9180329 100644 --- a/packages/imagekit-editor-dev/src/components/header/index.tsx +++ b/packages/imagekit-editor-dev/src/components/header/index.tsx @@ -116,7 +116,9 @@ export const Header = ({ onClose, exportOptions }: HeaderProps) => { (image) => image.url === currentImage, ) exportOption.onClick(images, { + // biome-ignore lint/style/noNonNullAssertion: url: cImage!.url, + // biome-ignore lint/style/noNonNullAssertion: file: cImage!.file, }) }} @@ -157,7 +159,9 @@ export const Header = ({ onClose, exportOptions }: HeaderProps) => { (image) => image.url === currentImage, ) option.onClick(images, { + // biome-ignore lint/style/noNonNullAssertion: url: cImage!.url, + // biome-ignore lint/style/noNonNullAssertion: file: cImage!.file, }) }} diff --git a/packages/imagekit-editor-dev/src/components/sidebar/sortable-transformation-item.tsx b/packages/imagekit-editor-dev/src/components/sidebar/sortable-transformation-item.tsx index 74ec731..427749f 100644 --- a/packages/imagekit-editor-dev/src/components/sidebar/sortable-transformation-item.tsx +++ b/packages/imagekit-editor-dev/src/components/sidebar/sortable-transformation-item.tsx @@ -1,43 +1,43 @@ import { Box, + Flex, HStack, Icon, + IconButton, + Input, Menu, MenuButton, MenuItem, MenuList, + Tag, Text, Tooltip, - Input, - Tag, - Flex, - IconButton, useColorModeValue, -} from "@chakra-ui/react"; -import { useState, useEffect, useRef } from "react"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { PiArrowDown } from "@react-icons/all-files/pi/PiArrowDown"; -import { PiArrowUp } from "@react-icons/all-files/pi/PiArrowUp"; -import { PiDotsSixVerticalBold } from "@react-icons/all-files/pi/PiDotsSixVerticalBold"; -import { PiDotsThreeVertical } from "@react-icons/all-files/pi/PiDotsThreeVertical"; -import { PiEye } from "@react-icons/all-files/pi/PiEye"; -import { PiEyeSlash } from "@react-icons/all-files/pi/PiEyeSlash"; -import { PiPencilSimple } from "@react-icons/all-files/pi/PiPencilSimple"; -import { PiPlus } from "@react-icons/all-files/pi/PiPlus"; -import { PiTrash } from "@react-icons/all-files/pi/PiTrash"; -import { RxTransform } from "@react-icons/all-files/rx/RxTransform"; -import { PiCopy } from "@react-icons/all-files/pi/PiCopy"; -import { PiCursorText } from "@react-icons/all-files/pi/PiCursorText"; -import { RiCheckFill } from "@react-icons/all-files/ri/RiCheckFill"; -import { RiCloseFill } from "@react-icons/all-files/ri/RiCloseFill"; -import { type Transformation, useEditorStore } from "../../store"; -import Hover from "../common/Hover"; +} from "@chakra-ui/react" +import { useSortable } from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { PiArrowDown } from "@react-icons/all-files/pi/PiArrowDown" +import { PiArrowUp } from "@react-icons/all-files/pi/PiArrowUp" +import { PiCopy } from "@react-icons/all-files/pi/PiCopy" +import { PiCursorText } from "@react-icons/all-files/pi/PiCursorText" +import { PiDotsSixVerticalBold } from "@react-icons/all-files/pi/PiDotsSixVerticalBold" +import { PiDotsThreeVertical } from "@react-icons/all-files/pi/PiDotsThreeVertical" +import { PiEye } from "@react-icons/all-files/pi/PiEye" +import { PiEyeSlash } from "@react-icons/all-files/pi/PiEyeSlash" +import { PiPencilSimple } from "@react-icons/all-files/pi/PiPencilSimple" +import { PiPlus } from "@react-icons/all-files/pi/PiPlus" +import { PiTrash } from "@react-icons/all-files/pi/PiTrash" +import { RiCheckFill } from "@react-icons/all-files/ri/RiCheckFill" +import { RiCloseFill } from "@react-icons/all-files/ri/RiCloseFill" +import { RxTransform } from "@react-icons/all-files/rx/RxTransform" +import { useEffect, useRef, useState } from "react" +import { type Transformation, useEditorStore } from "../../store" +import Hover from "../common/Hover" -export type TransformationPosition = "inplace" | number; +export type TransformationPosition = "inplace" | number interface SortableTransformationItemProps { - transformation: Transformation; + transformation: Transformation } export const SortableTransformationItem = ({ @@ -52,7 +52,7 @@ export const SortableTransformationItem = ({ isDragging, } = useSortable({ id: transformation.id, - }); + }) const { transformations, @@ -66,7 +66,7 @@ export const SortableTransformationItem = ({ _internalState, addTransformation, updateTransformation, - } = useEditorStore(); + } = useEditorStore() const style = transform ? { @@ -74,33 +74,33 @@ export const SortableTransformationItem = ({ transition, opacity: isDragging ? 0.5 : 1, } - : undefined; + : undefined - const isVisible = visibleTransformations[transformation.id]; + const isVisible = visibleTransformations[transformation.id] const isEditting = _internalState.transformationToEdit?.position === "inplace" && - _internalState.transformationToEdit?.transformationId === transformation.id; + _internalState.transformationToEdit?.transformationId === transformation.id - const [isRenaming, setIsRenaming] = useState(false); - const renameInputRef = useRef(null); - const renamingBoxRef = useRef(null); + const [isRenaming, setIsRenaming] = useState(false) + const renameInputRef = useRef(null) + const renamingBoxRef = useRef(null) - const baseIconColor = useColorModeValue("gray.600", "gray.300"); + const baseIconColor = useColorModeValue("gray.600", "gray.300") useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { - const renamingBox = renamingBoxRef.current; + const renamingBox = renamingBoxRef.current if (renamingBox && !renamingBox.contains(event.target as Node)) { - setIsRenaming(false); + setIsRenaming(false) } - }; + } - document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("mousedown", handleClickOutside) return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); + document.removeEventListener("mousedown", handleClickOutside) + } + }, []) return ( @@ -119,19 +119,19 @@ export const SortableTransformationItem = ({ minH="8" alignItems="center" style={style} - onClick={(e) => { - _setSidebarState("config"); - _setSelectedTransformationKey(transformation.key); - _setTransformationToEdit(transformation.id, "inplace"); + onClick={(_e) => { + _setSidebarState("config") + _setSelectedTransformationKey(transformation.key) + _setTransformationToEdit(transformation.id, "inplace") }} onDoubleClick={(e) => { - e.stopPropagation(); - setIsRenaming(true); + e.stopPropagation() + setIsRenaming(true) }} {...attributes} {...listeners} > - {(isHover && !isRenaming) ? ( + {isHover && !isRenaming ? ( { if (e.key === "Enter") { - const newName = renameInputRef.current?.value.trim(); + const newName = renameInputRef.current?.value.trim() if (newName && newName.length > 0) { updateTransformation(transformation.id, { ...transformation, name: newName, - }); + }) } - setIsRenaming(false); + setIsRenaming(false) } else if (e.key === "Escape") { - setIsRenaming(false); + setIsRenaming(false) } }} variant="flushed" @@ -183,14 +183,14 @@ export const SortableTransformationItem = ({ variant="ghost" color={baseIconColor} onClick={() => { - const newName = renameInputRef.current?.value.trim(); + const newName = renameInputRef.current?.value.trim() if (newName && newName.length > 0) { updateTransformation(transformation.id, { ...transformation, name: newName, - }); + }) } - setIsRenaming(false); + setIsRenaming(false) }} /> { - setIsRenaming(false); + setIsRenaming(false) }} /> @@ -230,8 +230,8 @@ export const SortableTransformationItem = ({ > { - e.stopPropagation(); - toggleTransformationVisibility(transformation.id); + e.stopPropagation() + toggleTransformationVisibility(transformation.id) }} > } onClick={(e) => { - e.stopPropagation(); - _setSidebarState("type"); - _setTransformationToEdit(transformation.id, "above"); + e.stopPropagation() + _setSidebarState("type") + _setTransformationToEdit(transformation.id, "above") }} > Add transformation before @@ -274,9 +274,9 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); - _setSidebarState("type"); - _setTransformationToEdit(transformation.id, "below"); + e.stopPropagation() + _setSidebarState("type") + _setTransformationToEdit(transformation.id, "below") }} > Add transformation after @@ -284,18 +284,18 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); + e.stopPropagation() const currentIndex = transformations.findIndex( (t) => t.id === transformation.id, - ); + ) const transformationId = addTransformation( { ...transformation, }, currentIndex + 1, - ); - _setSidebarState("config"); - _setTransformationToEdit(transformationId, "inplace"); + ) + _setSidebarState("config") + _setTransformationToEdit(transformationId, "inplace") }} > Duplicate @@ -303,10 +303,10 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); - _setSidebarState("config"); - _setSelectedTransformationKey(transformation.key); - _setTransformationToEdit(transformation.id, "inplace"); + e.stopPropagation() + _setSidebarState("config") + _setSelectedTransformationKey(transformation.key) + _setTransformationToEdit(transformation.id, "inplace") }} > Edit transformation @@ -314,11 +314,11 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); - setIsRenaming(true); - _setSidebarState("config"); - _setSelectedTransformationKey(transformation.key); - _setTransformationToEdit(transformation.id, "inplace"); + e.stopPropagation() + setIsRenaming(true) + _setSidebarState("config") + _setSelectedTransformationKey(transformation.key) + _setTransformationToEdit(transformation.id, "inplace") }} > Rename @@ -326,13 +326,13 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); + e.stopPropagation() const currentIndex = transformations.findIndex( (t) => t.id === transformation.id, - ); + ) if (currentIndex > 0) { - const targetId = transformations[currentIndex - 1].id; - moveTransformation(transformation.id, targetId); + const targetId = transformations[currentIndex - 1].id + moveTransformation(transformation.id, targetId) } }} isDisabled={ @@ -346,13 +346,13 @@ export const SortableTransformationItem = ({ } onClick={(e) => { - e.stopPropagation(); + e.stopPropagation() const currentIndex = transformations.findIndex( (t) => t.id === transformation.id, - ); + ) if (currentIndex < transformations.length - 1) { - const targetId = transformations[currentIndex + 1].id; - moveTransformation(transformation.id, targetId); + const targetId = transformations[currentIndex + 1].id + moveTransformation(transformation.id, targetId) } }} isDisabled={ @@ -368,15 +368,15 @@ export const SortableTransformationItem = ({ icon={} color="red.500" onClick={(e) => { - e.stopPropagation(); - removeTransformation(transformation.id); + e.stopPropagation() + removeTransformation(transformation.id) if ( _internalState.selectedTransformationKey === transformation.key ) { - _setSidebarState("none"); - _setSelectedTransformationKey(null); - _setTransformationToEdit(null); + _setSidebarState("none") + _setSelectedTransformationKey(null) + _setTransformationToEdit(null) } }} > @@ -389,5 +389,5 @@ export const SortableTransformationItem = ({ )} - ); -}; + ) +} diff --git a/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx b/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx index c234e99..08e5856 100644 --- a/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx +++ b/packages/imagekit-editor-dev/src/components/sidebar/transformation-config-sidebar.tsx @@ -39,6 +39,7 @@ import { PiInfo } from "@react-icons/all-files/pi/PiInfo" import { PiX } from "@react-icons/all-files/pi/PiX" import startCase from "lodash/startCase" import { useEffect, useMemo } from "react" +import type { ColorPickerProps } from "react-best-gradient-color-picker" import { Controller, type SubmitHandler, useForm } from "react-hook-form" import Select from "react-select" import CreateableSelect from "react-select/creatable" @@ -50,17 +51,27 @@ import { isStepAligned } from "../../utils" import AnchorField from "../common/AnchorField" import CheckboxCardField from "../common/CheckboxCardField" import ColorPickerField from "../common/ColorPickerField" -import GradientPicker, { GradientPickerState } from "../common/GradientPicker" +import RadiusInputField, { + type RadiusErrors, + type RadiusState, +} from "../common/CornerRadiusInput" +import DistortPerspectiveInput, { + type PerspectiveErrors, + type PerspectiveObject, +} from "../common/DistortPerspectiveInput" +import GradientPicker, { + type GradientPickerState, +} from "../common/GradientPicker" +import PaddingInputField, { + type PaddingErrors, + type PaddingState, +} from "../common/PaddingInput" import RadioCardField from "../common/RadioCardField" +import ZoomInput from "../common/ZoomInput" import { SidebarBody } from "./sidebar-body" import { SidebarFooter } from "./sidebar-footer" import { SidebarHeader } from "./sidebar-header" import { SidebarRoot } from "./sidebar-root" -import { ColorPickerProps } from "react-best-gradient-color-picker" -import PaddingInputField, { PaddingState } from "../common/PaddingInput" -import ZoomInput from "../common/ZoomInput" -import DistortPerspectiveInput, { PerspectiveObject } from "../common/DistortPerspectiveInput" -import RadiusInputField, { RadiusState } from "../common/CornerRadiusInput" export const TransformationConfigSidebar: React.FC = () => { const { @@ -87,7 +98,10 @@ export const TransformationConfigSidebar: React.FC = () => { ) }, [_internalState.selectedTransformationKey]) - const transformationToEdit = _internalState.transformationToEdit + const transformationToEdit = _internalState.transformationToEdit as { + transformationId: string + position: "inplace" + } const editedTransformation = useMemo(() => { if (!transformationToEdit) return undefined @@ -97,7 +111,9 @@ export const TransformationConfigSidebar: React.FC = () => { ) }, [transformations, transformationToEdit]) - const editedTransformationValue = editedTransformation?.value as Record | undefined + const editedTransformationValue = editedTransformation?.value as + | Record + | undefined const defaultValues = useMemo(() => { if ( @@ -299,7 +315,15 @@ export const TransformationConfigSidebar: React.FC = () => { return true }) .map((field: TransformationField) => ( - + field.fieldType === type, + ) + } + > {field.label} @@ -423,6 +447,13 @@ export const TransformationConfigSidebar: React.FC = () => { fontSize="sm" {...register(field.name)} {...(field.fieldProps ?? {})} + defaultValue={ + field.fieldProps?.defaultValue as + | string + | number + | readonly string[] + | undefined + } /> ) : null} {field.fieldType === "textarea" ? ( @@ -445,30 +476,51 @@ export const TransformationConfigSidebar: React.FC = () => { { const raw = watch(field.name) - const n = Number(raw) + const n = Number( + String(raw).toUpperCase().replace(/^N/, "-"), + ) + const isNumberWithN = + typeof raw === "string" && + !Number.isNaN(n) && + raw.toUpperCase().startsWith("N") if (!Number.isFinite(n)) return - const { step, min, max } = field.fieldProps ?? {} + const { step, min, max, skipStepCheck } = + field.fieldProps ?? {} let v = n if (min !== undefined) v = Math.max(v, min) if (max !== undefined) v = Math.min(v, max) - if (step) { + if (!skipStepCheck && step) { v = Math.round(v / step) * step const dp = (String(step).split(".")[1] || "").length v = Number(v.toFixed(dp)) } - setValue(field.name, String(v)) + const finalValue = + v < 0 && isNumberWithN ? `N${Math.abs(v)}` : String(v) + setValue(field.name, finalValue) }} onChange={(e) => { const val = e.target.value + const numSafeVal = String(val) + .toUpperCase() + .replace(/^N/, "-") + const isNumberWithN = + typeof val === "string" && + !Number.isNaN(Number(numSafeVal)) && + val.toUpperCase().startsWith("N") if (val === "") { setValue(field.name, "") @@ -486,18 +538,23 @@ export const TransformationConfigSidebar: React.FC = () => { ) { setValue(field.name, "auto") } else if ( + !field.fieldProps?.skipStepCheck && field.fieldProps?.step && !isStepAligned(val, field.fieldProps?.step) ) { return } else if ( field.fieldProps?.min !== undefined && - Number(val) < field.fieldProps.min + Number(numSafeVal) < field.fieldProps.min ) { - setValue(field.name, field.fieldProps.min) + const finalVal = + field.fieldProps.min < 0 && isNumberWithN + ? `N${Math.abs(field.fieldProps.min)}` + : String(field.fieldProps.min) + setValue(field.name, finalVal) } else if ( field.fieldProps?.max !== undefined && - Number(val) > field.fieldProps.max + Number(numSafeVal) > field.fieldProps.max ) { setValue(field.name, field.fieldProps.max) } else { @@ -523,9 +580,19 @@ export const TransformationConfigSidebar: React.FC = () => { max={field.fieldProps?.max || 100} step={field.fieldProps?.step || 1} value={ - Number.isNaN(Number(watch(field.name))) + Number.isNaN( + Number( + String(watch(field.name)) + .toUpperCase() + .replace(/^N/, "-"), + ), + ) ? 0 - : Number(watch(field.name)) + : Number( + String(watch(field.name)) + .toUpperCase() + .replace(/^N/, "-"), + ) } defaultValue={field.fieldProps?.defaultValue as number} onChange={(val) => setValue(field.name, val.toString())} @@ -534,7 +601,7 @@ export const TransformationConfigSidebar: React.FC = () => { - + ) : null} @@ -583,7 +650,7 @@ export const TransformationConfigSidebar: React.FC = () => { setValue(field.name, value) trigger(field.name) }} - errors={errors} + errors={errors as PaddingErrors} name={field.name} {...field.fieldProps} value={watch(field.name) as Partial} @@ -593,7 +660,6 @@ export const TransformationConfigSidebar: React.FC = () => { setValue(field.name, value)} - defaultValue={field.fieldProps?.defaultValue as number ?? 100} {...field.fieldProps} /> ) : null} @@ -603,7 +669,7 @@ export const TransformationConfigSidebar: React.FC = () => { setValue(field.name, value) trigger(field.name) }} - errors={errors} + errors={errors as PerspectiveErrors} name={field.name} value={watch(field.name) as PerspectiveObject} {...field.fieldProps} @@ -615,7 +681,7 @@ export const TransformationConfigSidebar: React.FC = () => { setValue(field.name, value) trigger(field.name) }} - errors={errors} + errors={errors as RadiusErrors} name={field.name} value={watch(field.name) as Partial} {...field.fieldProps} diff --git a/packages/imagekit-editor-dev/src/components/toolbar/toolbar.tsx b/packages/imagekit-editor-dev/src/components/toolbar/toolbar.tsx index 67ac076..6e0eb8a 100644 --- a/packages/imagekit-editor-dev/src/components/toolbar/toolbar.tsx +++ b/packages/imagekit-editor-dev/src/components/toolbar/toolbar.tsx @@ -206,6 +206,7 @@ export const Toolbar: FC = ({ onAddImage, onSelectImage }) => { } isLoading={isSigning} onLoad={(event) => { + // biome-ignore lint/style/noNonNullAssertion: setImageDimensions(originalImageList[index]!.url, { width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight, diff --git a/packages/imagekit-editor-dev/src/schema/index.ts b/packages/imagekit-editor-dev/src/schema/index.ts index c13c501..b4d2f4a 100644 --- a/packages/imagekit-editor-dev/src/schema/index.ts +++ b/packages/imagekit-editor-dev/src/schema/index.ts @@ -11,21 +11,21 @@ import { RxFontItalic } from "@react-icons/all-files/rx/RxFontItalic" import { RxTextAlignCenter } from "@react-icons/all-files/rx/RxTextAlignCenter" import { RxTextAlignLeft } from "@react-icons/all-files/rx/RxTextAlignLeft" import { RxTextAlignRight } from "@react-icons/all-files/rx/RxTextAlignRight" -import { z } from "zod/v3" +import { type RefinementCtx, z } from "zod/v3" +import type { PerspectiveObject } from "../components/common/DistortPerspectiveInput" +import type { GradientPickerState } from "../components/common/GradientPicker" import { SIMPLE_OVERLAY_TEXT_REGEX, safeBtoa } from "../utils" import { aspectRatioValidator, colorValidator, commonNumberAndExpressionValidator, heightValidator, - overlayBlockExprValidator, layerXValidator, layerYValidator, optionalPositiveFloatNumberValidator, refineUnsharpenMask, widthValidator, } from "./transformation" -import { GradientPickerState } from "../components/common/GradientPicker" // Based on ImageKit's supported object list export const DEFAULT_FOCUS_OBJECTS = [ @@ -512,7 +512,7 @@ export const transformationSchema: TransformationSchema[] = [ isTransformation: true, transformationGroup: "focus", fieldProps: { - isCreatable: false, + defaultValue: 100, }, helpText: "Select the zoom level for the focus area. Higher zoom levels crop closer to the focus point.", @@ -633,7 +633,7 @@ export const transformationSchema: TransformationSchema[] = [ isTransformation: true, transformationGroup: "focus", fieldProps: { - isCreatable: false, + defaultValue: 100, }, helpText: "Select the zoom level for the focus area. Higher zoom levels crop closer to the focus point.", @@ -1037,7 +1037,7 @@ export const transformationSchema: TransformationSchema[] = [ isTransformation: true, transformationGroup: "focus", fieldProps: { - isCreatable: false, + defaultValue: 100, }, helpText: "Select the zoom level for the focus area. Higher zoom levels crop closer to the focus point.", @@ -1369,23 +1369,33 @@ export const transformationSchema: TransformationSchema[] = [ defaultTransformation: {}, schema: z .object({ - gradient: z.object({ - from: z.string().optional(), - to: z.string().optional(), - direction: z.union([ - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0).max(359), - z.string(), - ]).optional(), - stopPoint: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(1).max(100).optional(), - }).optional(), - gradientSwitch: z.coerce - .boolean({ - invalid_type_error: "Should be a boolean.", + gradient: z + .object({ + from: z.string().optional(), + to: z.string().optional(), + direction: z + .union([ + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0) + .max(359), + z.string(), + ]) + .optional(), + stopPoint: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(1) + .max(100) + .optional(), }) + .optional(), + gradientSwitch: z.coerce.boolean({ + invalid_type_error: "Should be a boolean.", + }), }) .refine( (val) => { @@ -1424,8 +1434,8 @@ export const transformationSchema: TransformationSchema[] = [ to: "#00000000", direction: "bottom", stopPoint: 100, - } - } + }, + }, }, ], }, @@ -1440,17 +1450,22 @@ export const transformationSchema: TransformationSchema[] = [ .object({ distort: z.coerce.boolean(), distortType: z.enum(["perspective", "arc"]).optional(), - distortPerspective: z.object({ - x1: z.union([z.literal(""), z.coerce.number()]), - y1: z.union([z.literal(""), z.coerce.number()]), - x2: z.union([z.literal(""), z.coerce.number()]), - y2: z.union([z.literal(""), z.coerce.number()]), - x3: z.union([z.literal(""), z.coerce.number()]), - y3: z.union([z.literal(""), z.coerce.number()]), - x4: z.union([z.literal(""), z.coerce.number()]), - y4: z.union([z.literal(""), z.coerce.number()]), - }).optional(), - distortArcDegree: z.coerce.number().min(-359).max(359).optional(), + distortPerspective: z + .object({ + x1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + }) + .optional(), + distortArcDegree: z + .string() + .regex(/^[-N]?\d+$/) + .optional(), }) .refine( (val) => { @@ -1465,7 +1480,10 @@ export const transformationSchema: TransformationSchema[] = [ message: "At least one value is required", path: [], }, - ), + ) + .superRefine((val, ctx) => { + validatePerspectiveDistort(val, ctx) + }), transformations: [ { label: "Distort", @@ -1496,7 +1514,8 @@ export const transformationSchema: TransformationSchema[] = [ fieldType: "distort-perspective-input", isTransformation: false, transformationGroup: "distort", - isVisible: ({ distort, distortType }) => distort === true && distortType === "perspective", + isVisible: ({ distort, distortType }) => + distort === true && distortType === "perspective", fieldProps: { defaultValue: { x1: "", @@ -1507,23 +1526,28 @@ export const transformationSchema: TransformationSchema[] = [ y3: "", x4: "", y4: "", - } - } + }, + }, }, { label: "Distortion Arc Degrees", name: "distortArcDegree", - fieldType: "input", + fieldType: "slider", isTransformation: true, transformationGroup: "distort", - isVisible: ({ distort, distortType }) => distort === true && distortType === "arc", + isVisible: ({ distort, distortType }) => + distort === true && distortType === "arc", helpText: "Enter the arc degree for the arc distortion effect.", - examples: ["15", "30", "45"], + examples: ["15", "30", "-45", "N50"], fieldProps: { - type: "number", - placeholder: "Arc Degrees", - } - } + min: -360, + max: 360, + step: 5, + defaultValue: "0", + inputType: "text", + skipStepCheck: true, + }, + }, ], }, { @@ -1694,51 +1718,65 @@ export const transformationSchema: TransformationSchema[] = [ defaultTransformation: {}, schema: z .object({ - radius: z.object({ - mode: z.enum(["uniform", "individual"]).optional(), - radius: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - z.object({ - topLeft: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - topRight: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - bottomRight: z.union([ + radius: z + .object({ + mode: z.enum(["uniform", "individual"]).optional(), + radius: z + .union([ z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + z.object({ + topLeft: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + topRight: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + bottomRight: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + bottomLeft: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), }), - ]), - bottomLeft: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - }), - ]).optional(), - }).optional(), + ]) + .optional(), + }) + .optional(), }) .refine( (val) => { @@ -1765,8 +1803,8 @@ export const transformationSchema: TransformationSchema[] = [ "Enter a positive integer for rounded corners or 'max' for a fully circular output.", examples: ["10", "max"], fieldProps: { - defaultValue: {} - } + defaultValue: {}, + }, }, ], }, @@ -2091,19 +2129,27 @@ export const transformationSchema: TransformationSchema[] = [ docsLink: "https://imagekit.io/docs/effects-and-enhancements#unsharp-mask---e-usm", defaultTransformation: {}, - schema: z.object({ - unsharpenMaskRadius: z.coerce.number().positive({ message: "Should be a positive floating point number." }), - unsharpenMaskSigma: z.coerce.number().positive({ message: "Should be a positive floating point number." }), - unsharpenMaskAmount: z.coerce.number().positive({ message: "Should be a positive floating point number." }), - unsharpenMaskThreshold: z.coerce.number().positive({ message: "Should be a positive floating point number." }), - }) - .refine( - (val) => { - if (Object.values(val).some((v) => v !== undefined && v !== null)) { - return true - } - return false + schema: z + .object({ + unsharpenMaskRadius: z.coerce.number().positive({ + message: "Should be a positive floating point number.", + }), + unsharpenMaskSigma: z.coerce.number().positive({ + message: "Should be a positive floating point number.", + }), + unsharpenMaskAmount: z.coerce.number().positive({ + message: "Should be a positive floating point number.", + }), + unsharpenMaskThreshold: z.coerce.number().positive({ + message: "Should be a positive floating point number.", }), + }) + .refine((val) => { + if (Object.values(val).some((v) => v !== undefined && v !== null)) { + return true + } + return false + }), transformations: [ { name: "unsharpenMaskRadius", @@ -2137,8 +2183,7 @@ export const transformationSchema: TransformationSchema[] = [ label: "Amount", isTransformation: false, transformationGroup: "unsharpenMask", - helpText: - "Sets the strength of the sharpening effect.", + helpText: "Sets the strength of the sharpening effect.", fieldProps: { defaultValue: "", }, @@ -2150,15 +2195,14 @@ export const transformationSchema: TransformationSchema[] = [ label: "Threshold", isTransformation: false, transformationGroup: "unsharpenMask", - helpText: - "Set the threshold value for the unsharpen mask.", + helpText: "Set the threshold value for the unsharpen mask.", fieldProps: { defaultValue: "", }, examples: ["0.1", "2", "0.8"], }, - ] - } + ], + }, ], }, { @@ -2636,15 +2680,16 @@ export const transformationSchema: TransformationSchema[] = [ defaultTransformation: {}, schema: z .object({ - dpr: - z.union([ + dpr: z + .union([ z.coerce .number({ invalid_type_error: "Should be a number.", }) .optional(), z.literal("auto"), - ]).optional(), + ]) + .optional(), }) .refine( (val) => { @@ -2710,38 +2755,51 @@ export const transformationSchema: TransformationSchema[] = [ innerAlignment: z .enum(["left", "right", "center"]) .default("center"), - padding: z.object({ - mode: z.enum(["uniform", "individual"]).optional(), - padding: z.union([ - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - z.object({ - top: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - right: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - bottom: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - left: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - }), - ]).optional(), - }) + padding: z + .object({ + mode: z.enum(["uniform", "individual"]).optional(), + padding: z + .union([ + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + z.object({ + top: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + right: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + bottom: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + left: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + }), + ]) + .optional(), + }) .optional(), opacity: z .union([ @@ -3040,13 +3098,14 @@ export const transformationSchema: TransformationSchema[] = [ .optional(), backgroundColor: z.string().optional(), dprEnabled: z.boolean().optional(), - dpr: z.union([ - z.coerce - .number({ + dpr: z + .union([ + z.coerce.number({ invalid_type_error: "Should be a number.", }), - z.literal("auto"), - ]).optional(), + z.literal("auto"), + ]) + .optional(), flip: z .array(z.enum(["horizontal", "vertical"]).optional()) .optional(), @@ -3095,20 +3154,32 @@ export const transformationSchema: TransformationSchema[] = [ gradientSwitch: z.coerce .boolean({ invalid_type_error: "Should be a boolean.", - }).optional(), - gradient: z.object({ - from: z.string().optional(), - to: z.string().optional(), - direction: z.union([ - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0).max(359), - z.string(), - ]).optional(), - stopPoint: z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(1).max(100).optional(), - }).optional(), + }) + .optional(), + gradient: z + .object({ + from: z.string().optional(), + to: z.string().optional(), + direction: z + .union([ + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0) + .max(359), + z.string(), + ]) + .optional(), + stopPoint: z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(1) + .max(100) + .optional(), + }) + .optional(), // Shadow properties shadow: z.coerce @@ -3147,63 +3218,83 @@ export const transformationSchema: TransformationSchema[] = [ // Distort distort: z.coerce.boolean(), distortType: z.enum(["perspective", "arc"]).optional(), - distortPerspective: z.object({ - x1: z.union([z.literal(""), z.coerce.number()]), - y1: z.union([z.literal(""), z.coerce.number()]), - x2: z.union([z.literal(""), z.coerce.number()]), - y2: z.union([z.literal(""), z.coerce.number()]), - x3: z.union([z.literal(""), z.coerce.number()]), - y3: z.union([z.literal(""), z.coerce.number()]), - x4: z.union([z.literal(""), z.coerce.number()]), - y4: z.union([z.literal(""), z.coerce.number()]), - }).optional(), - distortArcDegree: z.coerce.number().min(-359).max(359).optional(), + distortPerspective: z + .object({ + x1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y1: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y2: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y3: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + x4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + y4: z.union([z.literal(""), z.string().regex(/^[-N]?\d+$/)]), + }) + .optional(), + distortArcDegree: z + .string() + .regex(/^[-N]?\d+$/) + .optional(), + // Radius - radius: z.object({ - mode: z.enum(["uniform", "individual"]).optional(), - radius: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - z.object({ - topLeft: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - topRight: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - bottomRight: z.union([ - z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", - }), - ]), - bottomLeft: z.union([ + radius: z + .object({ + mode: z.enum(["uniform", "individual"]).optional(), + radius: z + .union([ z.literal("max"), - z.coerce.number({ - invalid_type_error: "Should be a number.", - }).min(0, { - message: "Negative values are not allowed.", + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + z.object({ + topLeft: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + topRight: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + bottomRight: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), + bottomLeft: z.union([ + z.literal("max"), + z.coerce + .number({ + invalid_type_error: "Should be a number.", + }) + .min(0, { + message: "Negative values are not allowed.", + }), + ]), }), - ]), - }), - ]).optional(), - }).optional(), + ]) + .optional(), + }) + .optional(), sharpenEnabled: z.coerce .boolean({ invalid_type_error: "Should be a boolean.", @@ -3218,11 +3309,15 @@ export const transformationSchema: TransformationSchema[] = [ .max(99) .optional(), unsharpenMask: z.coerce.boolean().optional(), - unsharpenMaskRadius: optionalPositiveFloatNumberValidator.optional(), + unsharpenMaskRadius: + optionalPositiveFloatNumberValidator.optional(), unsharpenMaskSigma: optionalPositiveFloatNumberValidator.optional(), - unsharpenMaskAmount: optionalPositiveFloatNumberValidator.optional(), - unsharpenMaskThreshold: optionalPositiveFloatNumberValidator.optional(), - }).superRefine(refineUnsharpenMask) + unsharpenMaskAmount: + optionalPositiveFloatNumberValidator.optional(), + unsharpenMaskThreshold: + optionalPositiveFloatNumberValidator.optional(), + }) + .superRefine((val, ctx) => refineUnsharpenMask(val, ctx)) .refine( (val) => { return Object.values(val).some( @@ -3268,6 +3363,8 @@ export const transformationSchema: TransformationSchema[] = [ } } } + + validatePerspectiveDistort(val, ctx) }), transformations: [ { @@ -3350,10 +3447,19 @@ export const transformationSchema: TransformationSchema[] = [ transformationGroup: "imageLayer", fieldProps: { positions: [ - "center", "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right", + "center", + "top", + "bottom", + "left", + "right", + "top_left", + "top_right", + "bottom_left", + "bottom_right", ], }, - isVisible: ({ focus, crop }) => focus === "anchor" && crop === "cm-extract", + isVisible: ({ focus, crop }) => + focus === "anchor" && crop === "cm-extract", }, // Only for pad_resize crop mode { @@ -3363,9 +3469,7 @@ export const transformationSchema: TransformationSchema[] = [ isTransformation: true, transformationGroup: "imageLayer", fieldProps: { - positions: [ - "center", "top", "bottom", "left", "right", - ], + positions: ["center", "top", "bottom", "left", "right"], }, isVisible: ({ crop }) => crop === "cm-pad_resize", }, @@ -3441,7 +3545,8 @@ export const transformationSchema: TransformationSchema[] = [ fieldType: "input", isTransformation: true, transformationGroup: "imageLayer", - helpText: "Vertical center position of the overlay image. Use an integer or expression.", + helpText: + "Vertical center position of the overlay image. Use an integer or expression.", examples: ["200", "ih_mul_0.5"], isVisible: ({ focus, coordinateMethod }) => focus === "coordinates" && coordinateMethod === "center", @@ -3453,7 +3558,7 @@ export const transformationSchema: TransformationSchema[] = [ isTransformation: true, transformationGroup: "imageLayer", fieldProps: { - isCreatable: false, + defaultValue: 100, }, helpText: "Select the zoom level for the focus area. Higher zoom levels crop closer to the focus point.", @@ -3544,8 +3649,8 @@ export const transformationSchema: TransformationSchema[] = [ "Set the corner radius for the overlay image. Use 'max' for a circle or oval.", examples: ["10", "max"], fieldProps: { - defaultValue: {} - } + defaultValue: {}, + }, }, { label: "Flip", @@ -3656,7 +3761,8 @@ export const transformationSchema: TransformationSchema[] = [ fieldProps: { defaultValue: "", }, - helpText: "Enter the width of the border or expression of the overlay image.", + helpText: + "Enter the width of the border or expression of the overlay image.", examples: ["10", "ch_div_2"], }, { @@ -3750,8 +3856,7 @@ export const transformationSchema: TransformationSchema[] = [ label: "Amount", isTransformation: false, transformationGroup: "imageLayer", - helpText: - "Sets the strength of the sharpening effect.", + helpText: "Sets the strength of the sharpening effect.", fieldProps: { defaultValue: "", }, @@ -3764,8 +3869,7 @@ export const transformationSchema: TransformationSchema[] = [ label: "Threshold", isTransformation: false, transformationGroup: "imageLayer", - helpText: - "Set the threshold value for the unsharpen mask.", + helpText: "Set the threshold value for the unsharpen mask.", fieldProps: { defaultValue: "", }, @@ -3779,7 +3883,8 @@ export const transformationSchema: TransformationSchema[] = [ fieldType: "switch", isTransformation: false, transformationGroup: "imageLayer", - helpText: "Toggle to add a gradient overlay over the overlay image.", + helpText: + "Toggle to add a gradient overlay over the overlay image.", }, { label: "Apply Gradient", @@ -3795,8 +3900,8 @@ export const transformationSchema: TransformationSchema[] = [ to: "#00000000", direction: "bottom", stopPoint: 100, - } - } + }, + }, }, { label: "Shadow", @@ -3909,7 +4014,8 @@ export const transformationSchema: TransformationSchema[] = [ fieldType: "distort-perspective-input", isTransformation: false, transformationGroup: "imageLayer", - isVisible: ({ distort, distortType }) => distort === true && distortType === "perspective", + isVisible: ({ distort, distortType }) => + distort === true && distortType === "perspective", fieldProps: { defaultValue: { x1: "", @@ -3920,22 +4026,27 @@ export const transformationSchema: TransformationSchema[] = [ y3: "", x4: "", y4: "", - } - } + }, + }, }, { label: "Distortion Arc Degrees", name: "distortArcDegree", - fieldType: "input", + fieldType: "slider", isTransformation: true, transformationGroup: "imageLayer", - isVisible: ({ distort, distortType }) => distort === true && distortType === "arc", + isVisible: ({ distort, distortType }) => + distort === true && distortType === "arc", helpText: "Enter the arc degree for the arc distortion effect.", - examples: ["15", "30", "45"], + examples: ["15", "30", "-45", "N50"], fieldProps: { - type: "number", - placeholder: "Arc Degrees", - } + min: -360, + max: 360, + step: 5, + defaultValue: "0", + inputType: "text", + skipStepCheck: true, + }, }, ], }, @@ -4037,7 +4148,17 @@ export const transformationFormatters: Record< } }, focus: (values, transforms) => { - const { focus, focusAnchor, focusObject, x, y, xc, yc, coordinateMethod, zoom } = values + const { + focus, + focusAnchor, + focusObject, + x, + y, + xc, + yc, + coordinateMethod, + zoom, + } = values if (focus === "auto" || focus === "face") { transforms.focus = focus @@ -4058,7 +4179,12 @@ export const transformationFormatters: Record< if (yc) transforms.yc = yc } } - if (zoom !== undefined && zoom !== null && !isNaN(Number(zoom)) && zoom !== 0) { + if ( + zoom !== undefined && + zoom !== null && + !Number.isNaN(Number(zoom)) && + zoom !== 0 + ) { transforms.zoom = (zoom as number) / 100 } }, @@ -4160,18 +4286,21 @@ export const transformationFormatters: Record< const { padding, mode } = values.padding as Record if ( mode === "uniform" && - (typeof padding === "number" || - typeof padding === "string") + (typeof padding === "number" || typeof padding === "string") ) { overlayTransform.padding = padding - } else if (mode === "individual" && typeof padding === "object" && padding !== null) { + } else if ( + mode === "individual" && + typeof padding === "object" && + padding !== null + ) { const { top, right, bottom, left } = padding as { top: number right: number bottom: number left: number } - let paddingString: string; + let paddingString: string if (top === right && top === bottom && top === left) { paddingString = String(top) } else if (top === bottom && right === left) { @@ -4181,11 +4310,13 @@ export const transformationFormatters: Record< } overlayTransform.padding = paddingString } - if (typeof values.lineHeight === "number" || typeof values.lineHeight === "string") { + if ( + typeof values.lineHeight === "number" || + typeof values.lineHeight === "string" + ) { overlayTransform.lineHeight = values.lineHeight } - if (Array.isArray(values.flip) && values.flip.length > 0) { const flip = [] if (values.flip.includes("horizontal")) { @@ -4331,7 +4462,8 @@ export const transformationFormatters: Record< } if (values.unsharpenMask === true) { - overlayTransform["e-usm"] = `${values.unsharpenMaskRadius}-${values.unsharpenMaskSigma}-${values.unsharpenMaskAmount}-${values.unsharpenMaskThreshold}` + overlayTransform["e-usm"] = + `${values.unsharpenMaskRadius}-${values.unsharpenMaskSigma}-${values.unsharpenMaskAmount}-${values.unsharpenMaskThreshold}` } if ( values.trimEnabled === true && @@ -4359,7 +4491,8 @@ export const transformationFormatters: Record< } if ( values.borderWidth && - values.borderColor && typeof values.borderColor === "string" + values.borderColor && + typeof values.borderColor === "string" ) { overlayTransform.b = `${values.borderWidth}_${values.borderColor.replace(/^#/, "")}` } @@ -4495,20 +4628,31 @@ export const transformationFormatters: Record< } }, unsharpenMask: (values, transforms) => { - const { unsharpenMaskRadius, unsharpenMaskSigma, unsharpenMaskAmount, unsharpenMaskThreshold } = values as { + const { + unsharpenMaskRadius, + unsharpenMaskSigma, + unsharpenMaskAmount, + unsharpenMaskThreshold, + } = values as { unsharpenMaskRadius: number unsharpenMaskSigma: number unsharpenMaskAmount: number unsharpenMaskThreshold: number } - transforms["e-usm"] = `${unsharpenMaskRadius}-${unsharpenMaskSigma}-${unsharpenMaskAmount}-${unsharpenMaskThreshold}` + transforms["e-usm"] = + `${unsharpenMaskRadius}-${unsharpenMaskSigma}-${unsharpenMaskAmount}-${unsharpenMaskThreshold}` }, gradient: (values, transforms) => { - const { gradient, gradientSwitch } = values as { gradient: GradientPickerState; gradientSwitch: boolean } + const { gradient, gradientSwitch } = values as { + gradient: GradientPickerState + gradientSwitch: boolean + } if (gradientSwitch && gradient) { const { from, to, direction, stopPoint } = gradient - const isDefaultGradient = (from.toUpperCase() === "#FFFFFFFF" || from.toUpperCase() === "#FFFFFF") && - (to.toUpperCase() === "#00000000") && + const isDefaultGradient = + (from.toUpperCase() === "#FFFFFFFF" || + from.toUpperCase() === "#FFFFFF") && + to.toUpperCase() === "#00000000" && (direction === "bottom" || direction === 180) && stopPoint === 100 if (isDefaultGradient) { @@ -4517,7 +4661,7 @@ export const transformationFormatters: Record< const fromColor = from.replace("#", "") const toColor = to.replace("#", "") const stopPointDecimal = (stopPoint as number) / 100 - let gradientStr = `ld-${direction}_from-${fromColor}_to-${toColor}_sp-${stopPointDecimal}` + const gradientStr = `ld-${direction}_from-${fromColor}_to-${toColor}_sp-${stopPointDecimal}` transforms.gradient = gradientStr } } @@ -4527,32 +4671,107 @@ export const transformationFormatters: Record< const { distortType, distortPerspective, distortArcDegree } = values const distortPrefix = distortType === "perspective" ? "p" : "a" if (distortType === "perspective" && distortPerspective) { - const { x1, y1, x2, y2, x3, y3, x4, y4 } = distortPerspective as Record - const formattedCoords = [x1, y1, x2, y2, x3, y3, x4, y4].map(coord => coord.toString().replace(/^-/, "N")) - transforms["e-distort"] = `${distortPrefix}-${formattedCoords.join("_")}` - } else if (distortType === "arc" && distortArcDegree !== undefined && distortArcDegree !== null) { - transforms["e-distort"] = `${distortPrefix}-${distortArcDegree.toString().replace(/^-/, "N")}` + const { x1, y1, x2, y2, x3, y3, x4, y4 } = distortPerspective as Record< + string, + string + > + const formattedCoords = [x1, y1, x2, y2, x3, y3, x4, y4].map((coord) => + coord.toString().replace(/^-/, "N"), + ) + transforms["e-distort"] = + `${distortPrefix}-${formattedCoords.join("_")}` + } else if ( + distortType === "arc" && + distortArcDegree !== undefined && + distortArcDegree !== null + ) { + transforms["e-distort"] = + `${distortPrefix}-${distortArcDegree.toString().replace(/^-/, "N")}` } } }, radius: (values, transforms) => { if (values.radius) { const { radius, mode } = values.radius as Record - if (mode === "uniform" && (typeof radius === "number" || typeof radius === "string")) { + if ( + mode === "uniform" && + (typeof radius === "number" || typeof radius === "string") + ) { transforms.radius = radius - } else if (mode === "individual" && typeof radius === "object" && radius !== null) { + } else if ( + mode === "individual" && + typeof radius === "object" && + radius !== null + ) { const { topLeft, topRight, bottomRight, bottomLeft } = radius as { topLeft: number | "max" topRight: number | "max" bottomRight: number | "max" bottomLeft: number | "max" } - if (topLeft === topRight && topLeft === bottomRight && topLeft === bottomLeft) { + if ( + topLeft === topRight && + topLeft === bottomRight && + topLeft === bottomLeft + ) { transforms.radius = topLeft } else { transforms.radius = `${topLeft}_${topRight}_${bottomRight}_${bottomLeft}` } } } + }, +} + +function validatePerspectiveDistort( + value: { + distortPerspective?: PerspectiveObject + distort?: boolean + distortType?: string + } & Record, + ctx: RefinementCtx, +) { + const { distort, distortType, distortPerspective } = value + if (distort && distortType === "perspective" && distortPerspective) { + const perspective: PerspectiveObject = JSON.parse( + JSON.stringify(distortPerspective), + ) + const coords = Object.keys(perspective).reduce( + (acc, key) => { + const value = perspective[key as keyof typeof perspective] + if (!value) { + acc[key as keyof PerspectiveObject] = value + } + const numString = value.toUpperCase().replace(/^N/, "-") + acc[key as keyof PerspectiveObject] = parseInt(numString as string, 10) + return acc + }, + {} as Record, + ) + const allValuesProvided = Object.values(coords).every( + (v) => typeof v === "number" && !Number.isNaN(v), + ) + if (allValuesProvided) { + const { x1, y1, x2, y2, x3, y3, x4, y4 } = coords as Record< + keyof PerspectiveObject, + number + > + const isTopLeftValid = x1 < x2 && x1 < x3 && y1 < y3 && y1 < y4 + const isTopRightValid = x2 > x1 && x2 > x4 && y2 < y3 && y2 < y4 + const isBottomRightValid = x3 > x4 && x3 > x1 && y3 > y1 && y3 > y2 + const isBottomLeftValid = x4 < x3 && x4 < x2 && y4 > y1 && y4 > y2 + const isValid = + isTopLeftValid && + isTopRightValid && + isBottomRightValid && + isBottomLeftValid + if (!isValid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Perspective coordinates are invalid.", + path: ["distortPerspective"], + }) + } + } } } diff --git a/packages/imagekit-editor-dev/src/schema/transformation.ts b/packages/imagekit-editor-dev/src/schema/transformation.ts index d721ed2..100d55f 100644 --- a/packages/imagekit-editor-dev/src/schema/transformation.ts +++ b/packages/imagekit-editor-dev/src/schema/transformation.ts @@ -77,9 +77,7 @@ export const aspectRatioValidator = z.any().superRefine((val, ctx) => { }) }) -const layerXNumber = z.coerce - .string() - .regex(/^[N-]?\d+(\.\d{1,2})?$/) +const layerXNumber = z.coerce.string().regex(/^[N-]?\d+(\.\d{1,2})?$/) const layerXExpr = z .string() @@ -99,9 +97,7 @@ export const layerXValidator = z.any().superRefine((val, ctx) => { }) }) -const layerYNumber = z.coerce - .string() - .regex(/^[N-]?\d+(\.\d{1,2})?$/) +const layerYNumber = z.coerce.string().regex(/^[N-]?\d+(\.\d{1,2})?$/) const layerYExpr = z .string() @@ -121,7 +117,6 @@ export const layerYValidator = z.any().superRefine((val, ctx) => { }) }) - const commonNumber = z.coerce .number({ invalid_type_error: "Should be a number." }) .min(0, { @@ -129,24 +124,27 @@ const commonNumber = z.coerce }) const commonExpr = z .string() - .regex(/^(?:ih|bh|ch|iw|bw|cw)_(?:add|sub|mul|div|mod|pow)_(?:\d+(\.\d{1,2})?)$/, { - message: "String must be a valid expression string.", - }) - - -export const commonNumberAndExpressionValidator = z.any().superRefine((val, ctx) => { - if (commonNumber.safeParse(val).success) { - return - } - if (commonExpr.safeParse(val).success) { - return - } - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Must be a positive number or a valid expression string.", + .regex( + /^(?:ih|bh|ch|iw|bw|cw)_(?:add|sub|mul|div|mod|pow)_(?:\d+(\.\d{1,2})?)$/, + { + message: "String must be a valid expression string.", + }, + ) + +export const commonNumberAndExpressionValidator = z + .any() + .superRefine((val, ctx) => { + if (commonNumber.safeParse(val).success) { + return + } + if (commonExpr.safeParse(val).success) { + return + } + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Must be a positive number or a valid expression string.", + }) }) -}) - const overlayBlockExpr = z .string() @@ -154,7 +152,6 @@ const overlayBlockExpr = z message: "String must be a valid expression string.", }) - export const overlayBlockExprValidator = z.any().superRefine((val, ctx) => { if (commonNumber.safeParse(val).success) { return @@ -168,15 +165,24 @@ export const overlayBlockExprValidator = z.any().superRefine((val, ctx) => { }) }) - - - export const optionalPositiveFloatNumberValidator = z.preprocess( - (val) => (val === "" || val === undefined || val === null) ? undefined : val, - z.coerce.number().positive({ message: "Should be a positive floating point number." }).optional() + (val) => (val === "" || val === undefined || val === null ? undefined : val), + z.coerce + .number() + .positive({ message: "Should be a positive floating point number." }) + .optional(), ) -export const refineUnsharpenMask = (val: any, ctx: z.RefinementCtx) => { +export const refineUnsharpenMask = ( + val: { + unsharpenMask?: boolean + unsharpenMaskRadius?: number + unsharpenMaskSigma?: number + unsharpenMaskAmount?: number + unsharpenMaskThreshold?: number + }, + ctx: z.RefinementCtx, +) => { if (val.unsharpenMask === true) { if (!val.unsharpenMaskRadius) { ctx.addIssue({ @@ -207,4 +213,4 @@ export const refineUnsharpenMask = (val: any, ctx: z.RefinementCtx) => { }) } } -} \ No newline at end of file +}