diff --git a/src/assets/icons/color.png b/src/assets/icons/color.png new file mode 100644 index 0000000..f6bc357 Binary files /dev/null and b/src/assets/icons/color.png differ diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx new file mode 100644 index 0000000..b869dc4 --- /dev/null +++ b/src/components/ColorPicker.tsx @@ -0,0 +1,102 @@ +import { type FC, type InputHTMLAttributes, useState, useRef, useEffect } from 'react' +import { clx } from '../helpers/utils' +import Error from './Error' +import ColorfilterIcon from '@/assets/icons/color.png' + +type Props = { + label?: string + className?: string + error_text?: string + isNotRequired?: boolean + value?: string + onChange?: (value: string) => void +} & Omit, 'onChange' | 'value'> + +const ColorPicker: FC = (props: Props) => { + const [color, setColor] = useState(props.value || '#000000') + const colorInputRef = useRef(null) + + const inputClass = clx( + 'w-full bg-white h-10 text-black block pl-10 pr-10 text-xs rounded-xl border border-border', + props.readOnly && 'bg-gray-100 border-0 text-description', + props.className + ) + + const handleColorChange = (event: React.ChangeEvent) => { + const newColor = event.target.value + setColor(newColor) + props.onChange?.(newColor) + } + + const handleTextChange = (event: React.ChangeEvent) => { + const value = event.target.value + if (/^#[0-9A-Fa-f]{0,6}$/.test(value) || value === '') { + const finalColor = value || '#000000' + setColor(finalColor) + props.onChange?.(finalColor) + } + } + + const handleColorIconClick = () => { + if (!props.readOnly) { + colorInputRef.current?.click() + } + } + + useEffect(() => { + if (props.value !== undefined) { + setColor(props.value) + } + }, [props.value]) + + return ( +
+ + +
+ + + + +
+ + + + {props.error_text && ( + + )} +
+
+ ) +} + +export default ColorPicker + diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 656e9b0..f4552f3 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -78,6 +78,13 @@ const Input: FC = (props: Props) => { }, [search, props]) + const resolvedType = + props.type === 'password' + ? showPassword + ? 'text' + : 'password' + : props.type; + return (
diff --git a/src/pages/editor/components/canvas/drawingUtils.ts b/src/pages/editor/components/canvas/drawingUtils.ts index 9dede29..7365eb4 100644 --- a/src/pages/editor/components/canvas/drawingUtils.ts +++ b/src/pages/editor/components/canvas/drawingUtils.ts @@ -28,7 +28,7 @@ export const createDrawingObject = ({ height: Math.abs(height), fill: "#3b82f6", stroke: "#1e40af", - strokeWidth: 2, + strokeWidth: 0, shapeType: activeShape, }; } @@ -44,7 +44,7 @@ export const createDrawingObject = ({ height: radius * 2, fill: "#10b981", stroke: "#059669", - strokeWidth: 2, + strokeWidth: 0, }; } diff --git a/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx b/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx index a1e0219..86762de 100644 --- a/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx +++ b/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx @@ -1,6 +1,8 @@ -import { type FC, useRef, useState, useEffect } from "react"; +import { type FC, useState, useEffect, useMemo } from "react"; import Input from "@/components/Input"; import Select from "@/components/Select"; +import ColorPicker from "@/components/ColorPicker"; +import Button from "@/components/Button"; import { useEditorStore } from "@/pages/editor/store/editorStore"; import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore"; @@ -15,6 +17,17 @@ const fontWeightOptions = [ { label: "Light", value: "300" }, ]; +type TextFormState = { + text: string; + fontFamily: string; + fontWeight: string; + fontSize: number; + fill: string; + opacity: number; + letterSpacing: number; + wordSpacing: number; +}; + const TextInstruction: FC = () => { const { selectedObjectId, objects, updateObject } = useEditorStore(); const { defaults, updateDefaults } = useTextDefaultsStore(); @@ -24,39 +37,63 @@ const TextInstruction: FC = () => { : null; const isEditing = !!selectedObject; - const currentValues = isEditing - ? { - fontFamily: selectedObject.fontFamily || defaults.fontFamily, - fontWeight: selectedObject.fontWeight || defaults.fontWeight, - fontSize: selectedObject.fontSize || defaults.fontSize, - fill: selectedObject.fill || defaults.fill, - opacity: selectedObject.opacity ?? defaults.opacity, - letterSpacing: selectedObject.letterSpacing ?? defaults.letterSpacing, - wordSpacing: selectedObject.wordSpacing ?? defaults.wordSpacing, - } - : defaults; - - const handleChange = (field: string, value: string | number) => { + const baseValues = useMemo(() => { if (isEditing && selectedObject) { - updateObject(selectedObject.id, { [field]: value }); + return { + text: selectedObject.text || "", + fontFamily: selectedObject.fontFamily || defaults.fontFamily, + fontWeight: selectedObject.fontWeight || defaults.fontWeight, + fontSize: selectedObject.fontSize || defaults.fontSize, + fill: selectedObject.fill || defaults.fill, + opacity: selectedObject.opacity ?? defaults.opacity, + letterSpacing: selectedObject.letterSpacing ?? defaults.letterSpacing, + wordSpacing: selectedObject.wordSpacing ?? defaults.wordSpacing, + }; + } + return { text: "", ...defaults }; + }, [isEditing, selectedObject, defaults]); + + const [formState, setFormState] = useState(baseValues); + + useEffect(() => { + setFormState(baseValues); + }, [baseValues]); + + const handleChange = (field: keyof TextFormState, value: string | number) => { + if (isEditing && selectedObject) { + setFormState((prev) => ({ ...prev, [field]: value })); } else { - updateDefaults({ [field]: value }); + // text فقط برای حالت ویرایش است و نباید در defaults ذخیره شود + if (field !== "text") { + updateDefaults({ [field]: value }); + } } }; - const hexColor = (currentValues.fill || "#000000").replace("#", "").toUpperCase(); - const opacityValue = currentValues.opacity; - const colorInputRef = useRef(null); + const handleSave = () => { + if (isEditing && selectedObject) { + updateObject(selectedObject.id, formState); + } + }; + + const isDirty = isEditing && selectedObject + ? formState.text !== baseValues.text || + formState.fontFamily !== baseValues.fontFamily || + formState.fontWeight !== baseValues.fontWeight || + formState.fontSize !== baseValues.fontSize || + formState.fill !== baseValues.fill || + formState.opacity !== baseValues.opacity || + formState.letterSpacing !== baseValues.letterSpacing || + formState.wordSpacing !== baseValues.wordSpacing + : false; + + const opacityValue = formState.opacity; const [opacityInput, setOpacityInput] = useState(`${opacityValue}`); useEffect(() => { setOpacityInput(`${opacityValue}`); }, [opacityValue]); - const handleColorClick = () => { - colorInputRef.current?.click(); - }; - const handleOpacityChange = (e: React.ChangeEvent) => { const value = e.target.value.replace("%", "").trim(); setOpacityInput(value); @@ -66,20 +103,31 @@ const TextInstruction: FC = () => { const numValue = parseInt(opacityInput) || 0; const clampedValue = Math.min(100, Math.max(0, numValue)); setOpacityInput(`${clampedValue}`); - if (clampedValue !== opacityValue) { - handleChange("opacity", clampedValue); - } + handleChange("opacity", clampedValue); }; return (

متن

-
+ + {isEditing && ( +
+ +