,
+ property: keyof ShadowPropertiesProps["properties"],
+ ) => {
+ if (e.key === "Enter") {
+ applyProperty(e, property);
+ }
+ };
+
+ const addDefault = () => {
+ updateElements(
+ props.properties.shadowXOffset.map((property) => {
+ return {
+ id: property.nodeId,
+ shadowXOffset: 1,
+ shadowYOffset: 1,
+ shadowBlur: 1,
+ shadowColor: "#000000",
+ shadowOpacity: 1,
+ shadowSpread: 1,
+ };
+ }),
+ true,
+ );
+ };
+
+ const removeDefault = () => {
+ updateElements(
+ props.properties.shadowXOffset.map((property) => {
+ return {
+ id: property.nodeId,
+ shadowXOffset: null,
+ shadowYOffset: null,
+ shadowBlur: null,
+ shadowColor: null,
+ shadowOpacity: null,
+ shadowSpread: null,
+ };
+ }),
+ true,
+ );
+ };
+
+ return (
+
+ {hasValues ? (
+ <>
+
+
+ setShadowXOffset(e.target.value)}
+ onBlur={(e) => applyProperty(e, "shadowXOffset")}
+ onKeyUp={(e) => onKeyUp(e, "shadowXOffset")}
+ />
+
+
+ setShadowYOffset(e.target.value)}
+ onBlur={(e) => applyProperty(e, "shadowYOffset")}
+ onKeyUp={(e) => onKeyUp(e, "shadowYOffset")}
+ />
+
+
+
+
+ }
+ placeholder="Blur"
+ hasVariable={props.properties.shadowBlur[0].variable || false}
+ value={shadowBlurValue}
+ onChange={(e) => setShadowBlur(e.target.value)}
+ onBlur={(e) => applyProperty(e, "shadowBlur")}
+ onKeyUp={(e) => onKeyUp(e, "shadowBlur")}
+ />
+
+
+ setShadowSpread(e.target.value)}
+ onBlur={(e) => applyProperty(e, "shadowSpread")}
+ onKeyUp={(e) => onKeyUp(e, "shadowSpread")}
+ />
+
+
+ {colorValues.map((color) => (
+
+ ))}
+ >
+ ) : null}
+
+ );
+};
+
+type ColorLineProps = {
+ color: {
+ elementIds: string[];
+ value: string;
+ opacity: number;
+ colorVariable?: string | undefined;
+ opacityVariable?: string | undefined;
+ };
+};
+
+const ColorLine = ({ color }: ColorLineProps) => {
+ const updateElements = useEditorStore((state) => state.updateElements);
+
+ const [opacity, setOpacity] = useState(
+ color.opacityVariable
+ ? `{{${color.opacityVariable}}}`
+ : `${color.opacity * 100}%`,
+ );
+
+ const [colorValue, setColorValue] = useState(
+ color?.colorVariable ? `{{${color.colorVariable}}}` : color.value,
+ );
+
+ useEffect(() => {
+ setColorValue(
+ color?.colorVariable ? `{{${color.colorVariable}}}` : color.value,
+ );
+ }, [color.value, color.colorVariable]);
+
+ const applyColor = (newColor: string, saveToHistory = false) => {
+ const elementIds = color.elementIds;
+
+ updateElements(
+ elementIds.map((elementId) => ({
+ id: elementId,
+ shadowColor: newColor,
+ })),
+ saveToHistory,
+ );
+ };
+
+ const applyColorInput = (newColorValue: string) => {
+ const elementIds = color.elementIds;
+
+ const variableName = getVarFromString(newColorValue);
+
+ if (variableName && variableName.length > 0) {
+ updateElementsVariables(elementIds, "shadowColor", variableName);
+ return;
+ }
+
+ //Check if the color is hex color and valid
+ if (!/^#[0-9A-F]{6}$/i.test(newColorValue)) {
+ return;
+ }
+
+ updateElements(
+ elementIds.map((elementId) => {
+ const newVariablesWithoutProperty = getVariablesWithoutProperty(
+ "shadowColor",
+ elementId,
+ );
+
+ return {
+ id: elementId,
+ shadowColor: newColorValue,
+ variables: newVariablesWithoutProperty,
+ };
+ }),
+ true,
+ );
+ };
+
+ const applyOpacity = (opacity: string) => {
+ const elementIds = color.elementIds;
+
+ const variableName = getVarFromString(opacity);
+
+ if (variableName && variableName.length > 0) {
+ updateElementsVariables(elementIds, "shadowOpacity", variableName);
+ return;
+ }
+
+ const opacityValue = Number(opacity.replace("%", "")) / 100;
+
+ if (isNaN(opacityValue) || opacityValue < 0 || opacityValue > 1) {
+ return;
+ }
+
+ updateElements(
+ elementIds.map((elementId) => {
+ const newVariablesWithoutProperty = getVariablesWithoutProperty(
+ "shadowOpacity",
+ elementId,
+ );
+
+ return {
+ id: elementId,
+ shadowOpacity: opacityValue,
+ variables: newVariablesWithoutProperty,
+ };
+ }),
+ true,
+ );
+ };
+
+ return (
+
+ <>
+
+
+
+
e.stopPropagation()}>
+
+
+ }
+ hasVariable={color.colorVariable ? true : false}
+ placeholder="color hex"
+ value={colorValue}
+ onChange={(e) => setColorValue(e.target.value)}
+ onBlur={(e) => {
+ applyColorInput(e.target.value);
+ }}
+ onKeyUp={(e) => {
+ if (e.key === "Enter") {
+ applyColorInput(e.currentTarget.value);
+ }
+ }}
+ />
+
+
+
{
+ setOpacity(e.target.value);
+ }}
+ onBlur={(e) => {
+ applyOpacity(e.target.value);
+ }}
+ onKeyUp={(e) => {
+ if (e.key === "Enter") {
+ applyOpacity(e.currentTarget.value);
+ }
+ }}
+ />
+
+
+
+
+ {
+ applyColor(newColor.hex);
+ }}
+ onChangeComplete={(newColor) => {
+ applyColor(newColor.hex, true);
+ }}
+ />
+
+ >
+
+ );
+};
diff --git a/app/render-components/RectElementRendered.tsx b/app/render-components/RectElementRendered.tsx
index 8507129..3cba371 100644
--- a/app/render-components/RectElementRendered.tsx
+++ b/app/render-components/RectElementRendered.tsx
@@ -4,6 +4,29 @@ import { addAlphaToHex } from "~/utils/addAlphaToHex";
type RectElementProps = RectElement;
export const RectElementRendered = (props: RectElementProps) => {
+ //Create boxShadow string with border and shadow properties
+ const shadowValue =
+ props.shadowXOffset &&
+ props.shadowYOffset &&
+ props.shadowBlur &&
+ props.shadowSpread &&
+ props.shadowColor
+ ? `${props.shadowXOffset}px ${props.shadowYOffset}px ${
+ props.shadowBlur
+ }px ${props.shadowSpread}px ${addAlphaToHex(
+ props.shadowColor,
+ props.shadowOpacity || 1,
+ )}`
+ : undefined;
+ const borderValue =
+ props.borderColor && props.borderType && props.borderWidth
+ ? `0px 0px 0px ${props.borderWidth}px ${
+ props.borderType === "inside" ? "inset" : ""
+ } ${props.borderColor}`
+ : undefined;
+
+ const shadowList = [shadowValue, borderValue].filter(Boolean).join(", ");
+
return (
{
borderTopRightRadius: `${props.borderTopRightRadius}px`,
borderBottomLeftRadius: `${props.borderBottomLeftRadius}px`,
borderBottomRightRadius: `${props.borderBottomRightRadius}px`,
- boxShadow:
- props.borderColor && props.borderType && props.borderWidth
- ? `0px 0px 0px ${props.borderWidth}px ${
- props.borderType === "inside" ? "inset" : ""
- } ${props.borderColor}`
- : "none",
+ boxShadow: shadowList !== "" ? shadowList : "unset",
filter: props.blur ? `blur(${props.blur}px)` : "none",
}}
/>
diff --git a/app/stores/elementTypes.tsx b/app/stores/elementTypes.tsx
index 0764710..343dc9a 100644
--- a/app/stores/elementTypes.tsx
+++ b/app/stores/elementTypes.tsx
@@ -6,187 +6,211 @@ export type ElementType = z.infer;
//Base element properties that all elements share
const BaseElementSchema = z.object({
- id: z.string(),
- type: ElementType,
- x: z.number(),
- y: z.number(),
- width: z.number(),
- height: z.number(),
- rotate: z.number(),
+ id: z.string(),
+ type: ElementType,
+ x: z.number(),
+ y: z.number(),
+ width: z.number(),
+ height: z.number(),
+ rotate: z.number(),
});
export const RectElementSchema = BaseElementSchema.merge(
- z.object({
- type: z.enum(["rect"]),
- backgroundColor: z.string(),
- backgroundOpacity: z.number(),
- borderTopLeftRadius: z.number(),
- borderTopRightRadius: z.number(),
- borderBottomLeftRadius: z.number(),
- borderBottomRightRadius: z.number(),
- borderColor: z.string().nullish().default(null),
- borderWidth: z.number().nullish().default(null),
- borderType: z.enum(["inside", "outside"]).nullish().default(null),
- blur: z.number().nullish().default(null),
- }),
+ z.object({
+ type: z.enum(["rect"]),
+ backgroundColor: z.string(),
+ backgroundOpacity: z.number(),
+ borderTopLeftRadius: z.number(),
+ borderTopRightRadius: z.number(),
+ borderBottomLeftRadius: z.number(),
+ borderBottomRightRadius: z.number(),
+ borderColor: z.string().nullish().default(null),
+ borderWidth: z.number().nullish().default(null),
+ borderType: z.enum(["inside", "outside"]).nullish().default(null),
+ blur: z.number().nullish().default(null),
+ shadowXOffset: z.number().nullish().default(null),
+ shadowYOffset: z.number().nullish().default(null),
+ shadowBlur: z.number().nullish().default(null),
+ shadowColor: z.string().nullish().default(null),
+ shadowOpacity: z.number().nullish().default(null),
+ shadowSpread: z.number().nullish().default(null),
+ })
);
export type RectElement = z.infer;
//Text element properties
export const TextElementSchema = BaseElementSchema.merge(
- z.object({
- type: z.literal("text"),
- content: z.string(),
- fontSize: z.number(),
- color: z.string(),
- textColorOpacity: z.number(),
- fontFamily: z.string(),
- fontWeight: z.number(),
- fontStyle: z.enum(["normal", "italic"]),
- textAlign: z.enum(["left", "center", "right", "justify"]),
- textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]),
- lineHeight: z.number(),
- blur: z.number().nullish().default(null),
- textShadowXOffset: z.number().nullish().default(null),
- textShadowYOffset: z.number().nullish().default(null),
- textShadowBlur: z.number().nullish().default(null),
- textShadowColor: z.string().nullish().default(null),
- }),
+ z.object({
+ type: z.literal("text"),
+ content: z.string(),
+ fontSize: z.number(),
+ color: z.string(),
+ textColorOpacity: z.number(),
+ fontFamily: z.string(),
+ fontWeight: z.number(),
+ fontStyle: z.enum(["normal", "italic"]),
+ textAlign: z.enum(["left", "center", "right", "justify"]),
+ textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]),
+ lineHeight: z.number(),
+ blur: z.number().nullish().default(null),
+ textShadowXOffset: z.number().nullish().default(null),
+ textShadowYOffset: z.number().nullish().default(null),
+ textShadowBlur: z.number().nullish().default(null),
+ textShadowColor: z.string().nullish().default(null),
+ })
);
export type TextElement = z.infer;
//Image element properties
export const ImageElementSchema = BaseElementSchema.merge(
- z.object({
- type: z.literal("image"),
- src: z.string().nullable(),
- objectFit: z.enum(["fill", "contain", "cover", "none", "scale-down"]),
- borderTopLeftRadius: z.number(),
- borderTopRightRadius: z.number(),
- borderBottomLeftRadius: z.number(),
- borderBottomRightRadius: z.number(),
- borderColor: z.string().nullish().default(null),
- borderWidth: z.number().nullish().default(null),
- borderType: z.enum(["inside", "outside"]).nullish().default(null),
- blur: z.number().nullish().default(null),
- }),
+ z.object({
+ type: z.literal("image"),
+ src: z.string().nullable(),
+ objectFit: z.enum(["fill", "contain", "cover", "none", "scale-down"]),
+ borderTopLeftRadius: z.number(),
+ borderTopRightRadius: z.number(),
+ borderBottomLeftRadius: z.number(),
+ borderBottomRightRadius: z.number(),
+ borderColor: z.string().nullish().default(null),
+ borderWidth: z.number().nullish().default(null),
+ borderType: z.enum(["inside", "outside"]).nullish().default(null),
+ blur: z.number().nullish().default(null),
+ shadowXOffset: z.number().nullish().default(null),
+ shadowYOffset: z.number().nullish().default(null),
+ shadowBlur: z.number().nullish().default(null),
+ shadowColor: z.string().nullish().default(null),
+ shadowOpacity: z.number().nullish().default(null),
+ shadowSpread: z.number().nullish().default(null),
+ })
);
export type ImageElement = z.infer;
export const PageElementSchema = BaseElementSchema.merge(
- z.object({
- type: z.literal("page"),
- backgroundColor: z.string(),
- backgroundOpacity: z.number(),
- }),
+ z.object({
+ type: z.literal("page"),
+ backgroundColor: z.string(),
+ backgroundOpacity: z.number(),
+ })
);
export type PageElement = z.infer;
export const Variables = z.object({
- property: z.string(),
- name: z.string(),
+ property: z.string(),
+ name: z.string(),
});
export type Variables = z.infer;
export const RectElementWithVariables = RectElementSchema.merge(
- z.object({
- variables: z.array(Variables),
- }),
+ z.object({
+ variables: z.array(Variables),
+ })
);
export type RectElementWithVariables = z.infer;
export const TextElementWithVariables = TextElementSchema.merge(
- z.object({
- variables: z.array(Variables),
- }),
+ z.object({
+ variables: z.array(Variables),
+ })
);
export type TextElementWithVariables = z.infer;
export const ImageElementWithVariables = ImageElementSchema.merge(
- z.object({
- variables: z.array(Variables),
- }),
+ z.object({
+ variables: z.array(Variables),
+ })
);
export type ImageElementWithVariables = z.infer<
- typeof ImageElementWithVariables
+ typeof ImageElementWithVariables
>;
export const defaultRectElement: RectElement = {
- id: "",
- type: "rect",
- x: 0,
- y: 0,
- width: 100,
- height: 100,
- rotate: 0,
- backgroundColor: "#ff0000",
- backgroundOpacity: 1,
- borderTopLeftRadius: 0,
- borderTopRightRadius: 0,
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 0,
- borderColor: null,
- borderWidth: null,
- borderType: null,
- blur: null,
+ id: "",
+ type: "rect",
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ rotate: 0,
+ backgroundColor: "#ff0000",
+ backgroundOpacity: 1,
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 0,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ borderColor: null,
+ borderWidth: null,
+ borderType: null,
+ blur: null,
+ shadowXOffset: null,
+ shadowYOffset: null,
+ shadowBlur: null,
+ shadowColor: null,
+ shadowOpacity: null,
+ shadowSpread: null,
};
export const defaultTextElement: TextElement = {
- id: "",
- type: "text",
- x: 0,
- y: 0,
- rotate: 0,
- width: 100,
- height: 100,
- color: "#000000",
- textColorOpacity: 1,
- textTransform: "none",
- fontSize: 28,
- fontWeight: 400,
- fontStyle: "normal",
- fontFamily: "Open Sans",
- textAlign: "left",
- content: "Text",
- lineHeight: 1.5,
- blur: null,
- textShadowXOffset: null,
- textShadowYOffset: null,
- textShadowBlur: null,
- textShadowColor: null,
+ id: "",
+ type: "text",
+ x: 0,
+ y: 0,
+ rotate: 0,
+ width: 100,
+ height: 100,
+ color: "#000000",
+ textColorOpacity: 1,
+ textTransform: "none",
+ fontSize: 28,
+ fontWeight: 400,
+ fontStyle: "normal",
+ fontFamily: "Open Sans",
+ textAlign: "left",
+ content: "Text",
+ lineHeight: 1.5,
+ blur: null,
+ textShadowXOffset: null,
+ textShadowYOffset: null,
+ textShadowBlur: null,
+ textShadowColor: null,
};
export const defaultImageElement: ImageElement = {
- id: "",
- type: "image",
- x: 0,
- y: 0,
- rotate: 0,
- width: 100,
- height: 100,
- src: null,
- borderTopLeftRadius: 0,
- borderTopRightRadius: 0,
- borderBottomRightRadius: 0,
- borderBottomLeftRadius: 0,
- borderColor: null,
- borderWidth: null,
- borderType: null,
- objectFit: "cover",
- blur: null,
+ id: "",
+ type: "image",
+ x: 0,
+ y: 0,
+ rotate: 0,
+ width: 100,
+ height: 100,
+ src: null,
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ borderBottomLeftRadius: 0,
+ borderColor: null,
+ borderWidth: null,
+ borderType: null,
+ objectFit: "cover",
+ blur: null,
+ shadowXOffset: null,
+ shadowYOffset: null,
+ shadowBlur: null,
+ shadowColor: null,
+ shadowOpacity: null,
+ shadowSpread: null,
};
export const defaultElements = {
- rect: defaultRectElement,
- text: defaultTextElement,
- image: defaultImageElement,
+ rect: defaultRectElement,
+ text: defaultTextElement,
+ image: defaultImageElement,
};
//Element is type of RectElement, TextElement or ImageElement