Generated
+11
@@ -23,6 +23,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"page-flip": "^2.0.7",
|
"page-flip": "^2.0.7",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-colorful": "^5.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-i18next": "^16.3.0",
|
"react-i18next": "^16.3.0",
|
||||||
@@ -4556,6 +4557,16 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-colorful": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-date-object": {
|
"node_modules/react-date-object": {
|
||||||
"version": "2.1.9",
|
"version": "2.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"page-flip": "^2.0.7",
|
"page-flip": "^2.0.7",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-colorful": "^5.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-i18next": "^16.3.0",
|
"react-i18next": "^16.3.0",
|
||||||
|
|||||||
+176
-145
@@ -1,175 +1,206 @@
|
|||||||
import { type FC, type InputHTMLAttributes, useState, useRef, useEffect } from 'react'
|
import { type FC, type InputHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||||
import { clx } from '../helpers/utils'
|
import { HexAlphaColorPicker, HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
import Error from './Error'
|
import { clx } from "../helpers/utils";
|
||||||
import Input from './Input'
|
import Error from "./Error";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: string
|
label?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
error_text?: string
|
error_text?: string;
|
||||||
isNotRequired?: boolean
|
isNotRequired?: boolean;
|
||||||
value?: string
|
value?: string;
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void;
|
||||||
opacity?: number
|
opacity?: number;
|
||||||
onOpacityChange?: (value: number) => void
|
onOpacityChange?: (value: number) => void;
|
||||||
opacityLabel?: string
|
opacityLabel?: string;
|
||||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'>
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value">;
|
||||||
|
|
||||||
const ColorPicker: FC<Props> = (props: Props) => {
|
const ColorPicker: FC<Props> = (props: Props) => {
|
||||||
const [color, setColor] = useState<string>(props.value || '#000000')
|
const [color, setColor] = useState<string>(props.value || "#000000");
|
||||||
const [opacityDisplay, setOpacityDisplay] = useState<string>(`${props.opacity ?? 100}`)
|
const [opacityDisplay, setOpacityDisplay] = useState<string>(`${props.opacity ?? 100}`);
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const pickerOpenGuardRef = useRef(false)
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const showOpacity = props.onOpacityChange !== undefined
|
const showOpacity = props.onOpacityChange !== undefined;
|
||||||
|
|
||||||
const inputClass = clx(
|
const clampOpacity = (rawValue: number): number => Math.min(100, Math.max(0, rawValue));
|
||||||
'w-full bg-white h-[34px] text-black block pl-10 pr-10 text-xs rounded-xl border border-border',
|
|
||||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const normalizeHex = (value: string): string => {
|
||||||
const newColor = event.target.value
|
if (!value) return "#000000";
|
||||||
setColor(newColor)
|
const withHash = value.startsWith("#") ? value : `#${value}`;
|
||||||
props.onChange?.(newColor)
|
const shortMatch = /^#([0-9A-Fa-f]{3})$/.exec(withHash);
|
||||||
|
if (shortMatch) {
|
||||||
|
const [r, g, b] = shortMatch[1]!.split("");
|
||||||
|
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
||||||
}
|
}
|
||||||
|
const longMatch = /^#([0-9A-Fa-f]{6})$/.exec(withHash);
|
||||||
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
if (longMatch) {
|
||||||
const value = event.target.value
|
return `#${longMatch[1]}`.toLowerCase();
|
||||||
if (/^#[0-9A-Fa-f]{0,6}$/.test(value) || value === '') {
|
|
||||||
const finalColor = value || '#000000'
|
|
||||||
setColor(finalColor)
|
|
||||||
props.onChange?.(finalColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return "#000000";
|
||||||
|
};
|
||||||
|
|
||||||
const openNativeColorPicker = () => {
|
const toHexAlpha = (hex: string, opacity: number): string => {
|
||||||
if (props.readOnly) return
|
const normalizedHex = normalizeHex(hex);
|
||||||
if (pickerOpenGuardRef.current) return
|
const alpha = Math.round((clampOpacity(opacity) / 100) * 255)
|
||||||
pickerOpenGuardRef.current = true
|
.toString(16)
|
||||||
colorInputRef.current?.click()
|
.padStart(2, "0");
|
||||||
window.setTimeout(() => {
|
return `${normalizedHex}${alpha}`;
|
||||||
pickerOpenGuardRef.current = false
|
};
|
||||||
}, 400)
|
|
||||||
|
const fromHexAlpha = (hexAlpha: string): { hex: string; opacity: number } => {
|
||||||
|
const match = /^#([0-9A-Fa-f]{8})$/.exec(hexAlpha);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
hex: normalizeHex(hexAlpha),
|
||||||
|
opacity: 100,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
const rgbaHex = match[1]!.toLowerCase();
|
||||||
|
const rgb = `#${rgbaHex.slice(0, 6)}`;
|
||||||
|
const alphaHex = rgbaHex.slice(6, 8);
|
||||||
|
const opacity = Math.round((parseInt(alphaHex, 16) / 255) * 100);
|
||||||
|
return { hex: rgb, opacity: clampOpacity(opacity) };
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const inputClass = clx("w-full bg-white h-[34px] text-black block pl-10 pr-10 text-xs rounded-xl border border-border", props.readOnly && "bg-gray-100 border-0 text-description");
|
||||||
if (props.value !== undefined) {
|
|
||||||
setColor(props.value)
|
|
||||||
}
|
|
||||||
}, [props.value])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const handleColorChange = (newColor: string) => {
|
||||||
if (props.opacity !== undefined) {
|
const normalized = normalizeHex(newColor);
|
||||||
setOpacityDisplay(`${props.opacity}`)
|
setColor(normalized);
|
||||||
}
|
props.onChange?.(normalized);
|
||||||
}, [props.opacity])
|
};
|
||||||
|
|
||||||
const handleOpacityChange = (rawValue: string) => {
|
const togglePicker = () => {
|
||||||
setOpacityDisplay(rawValue)
|
if (props.readOnly) return;
|
||||||
const numValue = parseInt(rawValue, 10)
|
setIsOpen((prev) => !prev);
|
||||||
if (!Number.isNaN(numValue) && numValue >= 0 && numValue <= 100) {
|
};
|
||||||
props.onOpacityChange?.(numValue)
|
|
||||||
}
|
useEffect(() => {
|
||||||
|
if (props.value !== undefined) {
|
||||||
|
setColor(normalizeHex(props.value));
|
||||||
}
|
}
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
const handleOpacityBlur = (rawValue: string) => {
|
useEffect(() => {
|
||||||
const numValue = parseInt(rawValue, 10) || 0
|
if (props.opacity !== undefined) {
|
||||||
const clampedValue = Math.min(100, Math.max(0, numValue))
|
setOpacityDisplay(`${props.opacity}`);
|
||||||
setOpacityDisplay(`${clampedValue}`)
|
|
||||||
props.onOpacityChange?.(clampedValue)
|
|
||||||
}
|
}
|
||||||
|
}, [props.opacity]);
|
||||||
|
|
||||||
const colorField = (
|
useEffect(() => {
|
||||||
<div
|
if (!isOpen) return;
|
||||||
className={clx('w-full', showOpacity && 'flex-1')}
|
|
||||||
|
const handlePointerDownOutside = (event: MouseEvent) => {
|
||||||
|
if (!wrapperRef.current?.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousedown", handlePointerDownOutside);
|
||||||
|
return () => window.removeEventListener("mousedown", handlePointerDownOutside);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleOpacityChange = (rawValue: string) => {
|
||||||
|
setOpacityDisplay(rawValue);
|
||||||
|
const numValue = parseInt(rawValue, 10);
|
||||||
|
if (!Number.isNaN(numValue)) {
|
||||||
|
props.onOpacityChange?.(clampOpacity(numValue));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpacityBlur = (rawValue: string) => {
|
||||||
|
const numValue = parseInt(rawValue, 10) || 0;
|
||||||
|
const clampedValue = clampOpacity(numValue);
|
||||||
|
setOpacityDisplay(`${clampedValue}`);
|
||||||
|
props.onOpacityChange?.(clampedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAlphaPickerChange = (hexAlpha: string) => {
|
||||||
|
const { hex, opacity } = fromHexAlpha(hexAlpha);
|
||||||
|
setColor(hex);
|
||||||
|
setOpacityDisplay(`${opacity}`);
|
||||||
|
props.onChange?.(hex);
|
||||||
|
props.onOpacityChange?.(opacity);
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorField = (
|
||||||
|
<div className={clx("w-full", showOpacity && "flex-1")}>
|
||||||
|
<label className="text-xs">{props.label}</label>
|
||||||
|
|
||||||
|
<div className={clx("w-full relative mt-1 rounded-xl", !props.readOnly && "cursor-pointer")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={color}
|
||||||
|
onChange={(event) => handleColorChange(event.target.value)}
|
||||||
|
placeholder="#000000"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
title={!props.readOnly ? "کلیک برای باز کردن انتخابگر رنگ" : undefined}
|
||||||
|
onClick={togglePicker}
|
||||||
|
className={`${inputClass} ${props.readOnly ? "" : "cursor-pointer"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={clx("absolute top-0 bottom-0 my-auto left-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none", props.readOnly && "opacity-50")}
|
||||||
>
|
>
|
||||||
<label className='text-xs'>
|
{/* <img
|
||||||
{props.label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
'w-full relative mt-1 rounded-xl',
|
|
||||||
!props.readOnly && 'cursor-pointer'
|
|
||||||
)}
|
|
||||||
onClick={!props.readOnly ? openNativeColorPicker : undefined}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
value={color}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
placeholder='#000000'
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
title={
|
|
||||||
!props.readOnly
|
|
||||||
? 'کلیک: پالت رنگ — با Tab وارد فیلد شوید تا کد را تایپ کنید'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={`${inputClass} cursor-pointer`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className={clx(
|
|
||||||
'absolute top-0 bottom-0 my-auto left-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none',
|
|
||||||
props.readOnly && 'opacity-50'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* <img
|
|
||||||
src={ColorfilterIcon}
|
src={ColorfilterIcon}
|
||||||
alt=''
|
alt=''
|
||||||
className='size-7 select-none'
|
className='size-7 select-none'
|
||||||
/> */}
|
/> */}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={clx(
|
className={clx("absolute top-0 bottom-0 my-auto right-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none", props.readOnly && "opacity-50")}
|
||||||
'absolute top-0 bottom-0 my-auto right-1 flex items-center justify-center min-h-[34px] min-w-[34px] rounded-lg pointer-events-none',
|
>
|
||||||
props.readOnly && 'opacity-50'
|
<span className="block w-5 h-5 rounded-full border border-border shadow-sm" style={{ backgroundColor: color }} />
|
||||||
)}
|
</span>
|
||||||
>
|
|
||||||
<span
|
|
||||||
className='block w-5 h-5 rounded-full border border-border shadow-sm'
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<input
|
{isOpen && !props.readOnly && (
|
||||||
ref={colorInputRef}
|
<div className="absolute right-0 z-50 mt-2 w-full rounded-xl border border-border bg-white p-3 shadow-lg space-y-3">
|
||||||
type='color'
|
<div className="space-y-1">
|
||||||
value={color}
|
{showOpacity ? (
|
||||||
onChange={handleColorChange}
|
<HexAlphaColorPicker color={toHexAlpha(color, Number(opacityDisplay) || 100)} onChange={handleAlphaPickerChange} />
|
||||||
className='absolute opacity-0 pointer-events-none w-0 h-0'
|
) : (
|
||||||
/>
|
<HexColorPicker color={color} onChange={handleColorChange} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
)
|
<HexColorInput color={color} onChange={handleColorChange} prefixed className="h-9 max-w-[115px] flex-1 rounded-lg border border-border px-2 text-xs" />
|
||||||
|
{showOpacity && (
|
||||||
return (
|
<div className="flex-1">
|
||||||
<div className={clx('w-full', showOpacity && 'flex gap-3', props.className)}>
|
<input
|
||||||
{colorField}
|
type="number"
|
||||||
{showOpacity && (
|
min={0}
|
||||||
<div className='w-28 shrink-0'>
|
max={100}
|
||||||
<Input
|
value={opacityDisplay}
|
||||||
type='number'
|
onChange={(e) => handleOpacityChange(e.target.value)}
|
||||||
label={props.opacityLabel ?? 'شفافیت'}
|
onBlur={(e) => handleOpacityBlur(e.target.value)}
|
||||||
className='px-3'
|
className="h-9 w-full rounded-lg border border-border px-2 text-xs"
|
||||||
value={opacityDisplay}
|
title={props.opacityLabel ?? "شفافیت"}
|
||||||
onChange={(e) => handleOpacityChange(e.target.value)}
|
/>
|
||||||
onBlur={(e) => handleOpacityBlur(e.target.value)}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showOpacity && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{props.opacityLabel ?? "شفافیت"}: {opacityDisplay}%
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
{props.error_text && (
|
return (
|
||||||
<Error errorText={props.error_text} />
|
<div ref={wrapperRef} className={clx("w-full", props.className)}>
|
||||||
)}
|
{colorField}
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ColorPicker
|
{props.error_text && <Error errorText={props.error_text} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorPicker;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ZoomControls from "./ZoomControls";
|
|||||||
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
||||||
import { createScopedId } from "../store/editorStore.helpers";
|
import { createScopedId } from "../store/editorStore.helpers";
|
||||||
import { getKonvaGradientProps } from "../utils/gradient";
|
import { getKonvaGradientProps } from "../utils/gradient";
|
||||||
|
import { getColorWithOpacity } from "../utils/colorOpacity";
|
||||||
import { STICKER_DRAG_MIME } from "../types/IconTypes";
|
import { STICKER_DRAG_MIME } from "../types/IconTypes";
|
||||||
import { GALLERY_DRAG_MIME } from "../types/GalleryTypes";
|
import { GALLERY_DRAG_MIME } from "../types/GalleryTypes";
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +73,12 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
const currentPage = pages.find((page) => page.id === currentPageId);
|
const currentPage = pages.find((page) => page.id === currentPageId);
|
||||||
|
|
||||||
const bgColor =
|
const bgColor =
|
||||||
currentPage?.backgroundType === 'color' || currentPage?.backgroundType === 'gradient'
|
currentPage?.backgroundType === 'color'
|
||||||
|
? getColorWithOpacity(
|
||||||
|
currentPage.backgroundColor,
|
||||||
|
currentPage.backgroundOpacity ?? 100,
|
||||||
|
)
|
||||||
|
: currentPage?.backgroundType === 'gradient'
|
||||||
? currentPage.backgroundColor
|
? currentPage.backgroundColor
|
||||||
: '#ffffff';
|
: '#ffffff';
|
||||||
const bgGradient = currentPage?.backgroundGradient;
|
const bgGradient = currentPage?.backgroundGradient;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { useEditorStore } from '../store/editorStore'
|
import { useEditorStore } from '../store/editorStore'
|
||||||
import type { DisplayStyle, BackgroundType } from '../store/editorStore'
|
import type { DisplayStyle, BackgroundType } from '../store/editorStore'
|
||||||
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
|
||||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
import ColorsImage from '@/assets/images/colors.png'
|
|
||||||
import ColorPicker from '@/components/ColorPicker'
|
import ColorPicker from '@/components/ColorPicker'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
import { toCssLinearGradient } from '../utils/gradient'
|
import { toCssLinearGradient } from '../utils/gradient'
|
||||||
@@ -39,11 +38,11 @@ const SettingsPanel = () => {
|
|||||||
updateDocumentSettings,
|
updateDocumentSettings,
|
||||||
updateCurrentPageBackground,
|
updateCurrentPageBackground,
|
||||||
} = useEditorStore()
|
} = useEditorStore()
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const { mutateAsync: uploadFile, isPending: mediaLoading } = useSingleUpload()
|
const { mutateAsync: uploadFile, isPending: mediaLoading } = useSingleUpload()
|
||||||
const currentPage = pages.find((page) => page.id === currentPageId)
|
const currentPage = pages.find((page) => page.id === currentPageId)
|
||||||
const backgroundType = currentPage?.backgroundType ?? 'color'
|
const backgroundType = currentPage?.backgroundType ?? 'color'
|
||||||
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
|
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
|
||||||
|
const backgroundOpacity = currentPage?.backgroundOpacity ?? 100
|
||||||
const backgroundGradient = currentPage?.backgroundGradient ?? {
|
const backgroundGradient = currentPage?.backgroundGradient ?? {
|
||||||
from: '#ffffff',
|
from: '#ffffff',
|
||||||
to: '#e2e8f0',
|
to: '#e2e8f0',
|
||||||
@@ -234,30 +233,29 @@ const SettingsPanel = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Color Wheel */}
|
<div className="w-full mt-2">
|
||||||
<button
|
<ColorPicker
|
||||||
onClick={() => colorInputRef.current?.click()}
|
label="رنگ دلخواه"
|
||||||
className="w-9 h-9 rounded-xl border-2 border-transparent hover:scale-105 transition-all overflow-hidden relative"
|
|
||||||
title="رنگ دلخواه"
|
|
||||||
>
|
|
||||||
<img src={ColorsImage} alt="انتخاب رنگ" className="w-full h-full object-cover" />
|
|
||||||
<input
|
|
||||||
ref={colorInputRef}
|
|
||||||
type="color"
|
|
||||||
value={backgroundColor}
|
value={backgroundColor}
|
||||||
onChange={(e) => updateCurrentPageBackground({ backgroundColor: e.target.value })}
|
opacity={backgroundOpacity}
|
||||||
className="absolute inset-0 opacity-0 w-full h-full cursor-pointer"
|
onChange={(value) =>
|
||||||
|
updateCurrentPageBackground({ backgroundColor: value })
|
||||||
|
}
|
||||||
|
onOpacityChange={(value) =>
|
||||||
|
updateCurrentPageBackground({ backgroundOpacity: value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* نمایش رنگ انتخابشده */}
|
{/* نمایش رنگ انتخابشده */}
|
||||||
<div className="mt-3 flex items-center gap-2">
|
<div className="mt-3 flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-lg border border-border"
|
className="w-6 h-6 rounded-lg border border-border"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor, opacity: backgroundOpacity / 100 }}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
|
<span className="text-xs text-gray-500 font-mono">{backgroundColor}</span>
|
||||||
|
<span className="text-xs text-gray-400">{backgroundOpacity}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const createInitialPage = (name: string): Page => ({
|
|||||||
guides: [],
|
guides: [],
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
|
backgroundOpacity: 100,
|
||||||
backgroundGradient: {
|
backgroundGradient: {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
to: "#e2e8f0",
|
to: "#e2e8f0",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type PageBackgroundSettings = Pick<
|
|||||||
Page,
|
Page,
|
||||||
| "backgroundType"
|
| "backgroundType"
|
||||||
| "backgroundColor"
|
| "backgroundColor"
|
||||||
|
| "backgroundOpacity"
|
||||||
| "backgroundGradient"
|
| "backgroundGradient"
|
||||||
| "backgroundImageUrl"
|
| "backgroundImageUrl"
|
||||||
| "backgroundVideoUrl"
|
| "backgroundVideoUrl"
|
||||||
@@ -191,6 +192,7 @@ const DEFAULT_DOCUMENT_SETTINGS: DocumentSettings = {
|
|||||||
smartGuide: true,
|
smartGuide: true,
|
||||||
backgroundType: "color",
|
backgroundType: "color",
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
|
backgroundOpacity: 100,
|
||||||
backgroundGradient: {
|
backgroundGradient: {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
to: "#e2e8f0",
|
to: "#e2e8f0",
|
||||||
@@ -887,6 +889,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
guides: [...pageToDuplicate.guides],
|
guides: [...pageToDuplicate.guides],
|
||||||
backgroundType: pageToDuplicate.backgroundType,
|
backgroundType: pageToDuplicate.backgroundType,
|
||||||
backgroundColor: pageToDuplicate.backgroundColor,
|
backgroundColor: pageToDuplicate.backgroundColor,
|
||||||
|
backgroundOpacity: pageToDuplicate.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: pageToDuplicate.backgroundGradient,
|
backgroundGradient: pageToDuplicate.backgroundGradient,
|
||||||
backgroundImageUrl: pageToDuplicate.backgroundImageUrl,
|
backgroundImageUrl: pageToDuplicate.backgroundImageUrl,
|
||||||
backgroundVideoUrl: pageToDuplicate.backgroundVideoUrl,
|
backgroundVideoUrl: pageToDuplicate.backgroundVideoUrl,
|
||||||
@@ -1082,6 +1085,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
|||||||
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
page.backgroundType ?? fallbackSettings.backgroundType ?? "color",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
page.backgroundColor ?? fallbackSettings.backgroundColor ?? "#ffffff",
|
page.backgroundColor ?? fallbackSettings.backgroundColor ?? "#ffffff",
|
||||||
|
backgroundOpacity:
|
||||||
|
page.backgroundOpacity ?? fallbackSettings.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: page.backgroundGradient ??
|
backgroundGradient: page.backgroundGradient ??
|
||||||
fallbackSettings.backgroundGradient ?? {
|
fallbackSettings.backgroundGradient ?? {
|
||||||
from: "#ffffff",
|
from: "#ffffff",
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export type Page = {
|
|||||||
guides: PageGuide[];
|
guides: PageGuide[];
|
||||||
backgroundType: BackgroundType;
|
backgroundType: BackgroundType;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: LinearGradient;
|
backgroundGradient?: LinearGradient;
|
||||||
backgroundImageUrl: string;
|
backgroundImageUrl: string;
|
||||||
backgroundVideoUrl: string;
|
backgroundVideoUrl: string;
|
||||||
@@ -139,6 +140,7 @@ export type DocumentSettings = {
|
|||||||
smartGuide: boolean;
|
smartGuide: boolean;
|
||||||
backgroundType: BackgroundType;
|
backgroundType: BackgroundType;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: LinearGradient;
|
backgroundGradient?: LinearGradient;
|
||||||
backgroundImageUrl: string;
|
backgroundImageUrl: string;
|
||||||
backgroundVideoUrl: string;
|
backgroundVideoUrl: string;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type BookPageProps = {
|
|||||||
onLinkClick?: (linkUrl: string) => void;
|
onLinkClick?: (linkUrl: string) => void;
|
||||||
backgroundType?: 'color' | 'gradient' | 'image' | 'video';
|
backgroundType?: 'color' | 'gradient' | 'image' | 'video';
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -94,9 +95,10 @@ type BookPageProps = {
|
|||||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||||
*/
|
*/
|
||||||
const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
||||||
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, onDeferredEntranceReady, hideMediaLoadingOverlay = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
({ page, scale = 1, pageWidth, pageHeight, onLinkClick, backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl, entrancePhase = 'idle', disableEntranceAnimations = false, onDeferredEntranceReady, hideMediaLoadingOverlay = false, staticMedia = false, showPaperShadow = true, useHardPage = false, idSuffix = '' }, ref) => {
|
||||||
const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color';
|
const effectiveBackgroundType = backgroundType ?? page.backgroundType ?? 'color';
|
||||||
const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff';
|
const effectiveBackgroundColor = backgroundColor ?? page.backgroundColor ?? '#ffffff';
|
||||||
|
const effectiveBackgroundOpacity = backgroundOpacity ?? page.backgroundOpacity ?? 100;
|
||||||
const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient;
|
const effectiveBackgroundGradient = backgroundGradient ?? page.backgroundGradient;
|
||||||
const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? '';
|
const effectiveBackgroundImageUrl = backgroundImageUrl ?? page.backgroundImageUrl ?? '';
|
||||||
const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? '';
|
const effectiveBackgroundVideoUrl = backgroundVideoUrl ?? page.backgroundVideoUrl ?? '';
|
||||||
@@ -1034,7 +1036,7 @@ const BookPage = memo(forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
? {
|
? {
|
||||||
backgroundImage: toCssLinearGradient(effectiveBackgroundGradient),
|
backgroundImage: toCssLinearGradient(effectiveBackgroundGradient),
|
||||||
}
|
}
|
||||||
: { backgroundColor: effectiveBackgroundColor };
|
: { backgroundColor: getColorWithOpacity(effectiveBackgroundColor, effectiveBackgroundOpacity) };
|
||||||
|
|
||||||
const showLoadingOverlay = !hideMediaLoadingOverlay && !isMediaReady;
|
const showLoadingOverlay = !hideMediaLoadingOverlay && !isMediaReady;
|
||||||
|
|
||||||
|
|||||||
@@ -193,12 +193,13 @@ const BookViewer: FC<BookViewerProps> = ({ pages, catalogSize, documentSettings
|
|||||||
);
|
);
|
||||||
const backgroundType = documentSettings?.backgroundType ?? "color";
|
const backgroundType = documentSettings?.backgroundType ?? "color";
|
||||||
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
const backgroundColor = documentSettings?.backgroundColor ?? "#ffffff";
|
||||||
|
const backgroundOpacity = documentSettings?.backgroundOpacity ?? 100;
|
||||||
const backgroundGradient = documentSettings?.backgroundGradient;
|
const backgroundGradient = documentSettings?.backgroundGradient;
|
||||||
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? "";
|
const backgroundImageUrl = documentSettings?.backgroundImageUrl ?? "";
|
||||||
const backgroundVideoUrl = documentSettings?.backgroundVideoUrl ?? "";
|
const backgroundVideoUrl = documentSettings?.backgroundVideoUrl ?? "";
|
||||||
const pageBackgroundDefaults = useMemo(
|
const pageBackgroundDefaults = useMemo(
|
||||||
() => ({ backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl }),
|
() => ({ backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl }),
|
||||||
[backgroundType, backgroundColor, backgroundGradient, backgroundImageUrl, backgroundVideoUrl],
|
[backgroundType, backgroundColor, backgroundOpacity, backgroundGradient, backgroundImageUrl, backgroundVideoUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type PageData = {
|
|||||||
elements: EditorObject[];
|
elements: EditorObject[];
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ViewerDataPage = {
|
|||||||
name: string;
|
name: string;
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: {
|
backgroundGradient?: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@@ -203,6 +204,7 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
elements: objects,
|
elements: objects,
|
||||||
backgroundType: page.backgroundType,
|
backgroundType: page.backgroundType,
|
||||||
backgroundColor: page.backgroundColor,
|
backgroundColor: page.backgroundColor,
|
||||||
|
backgroundOpacity: page.backgroundOpacity,
|
||||||
backgroundGradient: page.backgroundGradient,
|
backgroundGradient: page.backgroundGradient,
|
||||||
backgroundImageUrl: page.backgroundImageUrl,
|
backgroundImageUrl: page.backgroundImageUrl,
|
||||||
backgroundVideoUrl: page.backgroundVideoUrl,
|
backgroundVideoUrl: page.backgroundVideoUrl,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { PageData } from "@/pages/viewer/types";
|
|||||||
export type PageBackgroundDefaults = {
|
export type PageBackgroundDefaults = {
|
||||||
backgroundType?: "color" | "gradient" | "image" | "video";
|
backgroundType?: "color" | "gradient" | "image" | "video";
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundOpacity?: number;
|
||||||
backgroundGradient?: { from: string; to: string; angle: number };
|
backgroundGradient?: { from: string; to: string; angle: number };
|
||||||
backgroundImageUrl?: string;
|
backgroundImageUrl?: string;
|
||||||
backgroundVideoUrl?: string;
|
backgroundVideoUrl?: string;
|
||||||
@@ -13,6 +14,7 @@ export function resolvePageBackground(page: PageData, defaults: PageBackgroundDe
|
|||||||
return {
|
return {
|
||||||
backgroundType: page.backgroundType ?? defaults.backgroundType ?? "color",
|
backgroundType: page.backgroundType ?? defaults.backgroundType ?? "color",
|
||||||
backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff",
|
backgroundColor: page.backgroundColor ?? defaults.backgroundColor ?? "#ffffff",
|
||||||
|
backgroundOpacity: page.backgroundOpacity ?? defaults.backgroundOpacity ?? 100,
|
||||||
backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 },
|
backgroundGradient: page.backgroundGradient ?? defaults.backgroundGradient ?? { from: "#ffffff", to: "#ffffff", angle: 0 },
|
||||||
backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "",
|
backgroundImageUrl: page.backgroundImageUrl ?? defaults.backgroundImageUrl ?? "",
|
||||||
backgroundVideoUrl: page.backgroundVideoUrl ?? defaults.backgroundVideoUrl ?? "",
|
backgroundVideoUrl: page.backgroundVideoUrl ?? defaults.backgroundVideoUrl ?? "",
|
||||||
|
|||||||
Reference in New Issue
Block a user