pixedi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.github/workflows/deploy.yml +54 -0
  2. package/.prettierr +9 -0
  3. package/.storybook/main.ts +16 -0
  4. package/.storybook/preview.ts +53 -0
  5. package/.vscode/extensions.json +6 -0
  6. package/README.md +75 -0
  7. package/eslint.config.js +22 -0
  8. package/index.html +13 -0
  9. package/package.json +57 -0
  10. package/pnpm-workspace.yaml +2 -0
  11. package/public/favicon.svg +1 -0
  12. package/src/app/App.tsx +10 -0
  13. package/src/app/entries/index.ts +145 -0
  14. package/src/app/providers/RouterProvider.tsx +6 -0
  15. package/src/app/providers/index.ts +1 -0
  16. package/src/app/router/AppRouter.tsx +12 -0
  17. package/src/app/router/index.ts +0 -0
  18. package/src/app/styles/index.css +4 -0
  19. package/src/features/gallery/GalleryPage.tsx +33 -0
  20. package/src/features/gallery/ImageEditPage.tsx +37 -0
  21. package/src/features/gallery/index.ts +2 -0
  22. package/src/main.tsx +10 -0
  23. package/src/shared/components/ImageCard/ImageCard.tsx +54 -0
  24. package/src/shared/components/ImageCard/index.ts +1 -0
  25. package/src/shared/components/ImageEditor/ImageEditor.tsx +48 -0
  26. package/src/shared/components/ImageEditor/assets/icons.tsx +114 -0
  27. package/src/shared/components/ImageEditor/constants.ts +118 -0
  28. package/src/shared/components/ImageEditor/crop/Crop.tsx +60 -0
  29. package/src/shared/components/ImageEditor/crop/CropBorders.tsx +28 -0
  30. package/src/shared/components/ImageEditor/crop/CropLines.tsx +8 -0
  31. package/src/shared/components/ImageEditor/crop/CropPointer.tsx +21 -0
  32. package/src/shared/components/ImageEditor/crop/CropPointers.tsx +44 -0
  33. package/src/shared/components/ImageEditor/crop/CropTools.tsx +138 -0
  34. package/src/shared/components/ImageEditor/crop/crop.css +180 -0
  35. package/src/shared/components/ImageEditor/crop/index.ts +8 -0
  36. package/src/shared/components/ImageEditor/crop/useCropInteraction.ts +230 -0
  37. package/src/shared/components/ImageEditor/eventBus.ts +8 -0
  38. package/src/shared/components/ImageEditor/frame/Frame.tsx +48 -0
  39. package/src/shared/components/ImageEditor/frame/frame.css +40 -0
  40. package/src/shared/components/ImageEditor/frame/index.ts +1 -0
  41. package/src/shared/components/ImageEditor/header/Header.tsx +81 -0
  42. package/src/shared/components/ImageEditor/header/header.css +27 -0
  43. package/src/shared/components/ImageEditor/header/index.ts +1 -0
  44. package/src/shared/components/ImageEditor/hooks/index.ts +5 -0
  45. package/src/shared/components/ImageEditor/hooks/useAbove.tsx +18 -0
  46. package/src/shared/components/ImageEditor/hooks/useBellow.tsx +18 -0
  47. package/src/shared/components/ImageEditor/hooks/useImageLoader.tsx +110 -0
  48. package/src/shared/components/ImageEditor/hooks/useImageProcessor.test.tsx +147 -0
  49. package/src/shared/components/ImageEditor/hooks/useImageProcessor.ts +225 -0
  50. package/src/shared/components/ImageEditor/hooks/useMobile.tsx +33 -0
  51. package/src/shared/components/ImageEditor/index.css +145 -0
  52. package/src/shared/components/ImageEditor/index.ts +1 -0
  53. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.test.tsx +126 -0
  54. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.tsx +107 -0
  55. package/src/shared/components/ImageEditor/provider/StoreContext.tsx +20 -0
  56. package/src/shared/components/ImageEditor/provider/initialState.ts +30 -0
  57. package/src/shared/components/ImageEditor/provider/useImageEditorContext.tsx +12 -0
  58. package/src/shared/components/ImageEditor/resize/ResizeTools.tsx +183 -0
  59. package/src/shared/components/ImageEditor/resize/index.ts +1 -0
  60. package/src/shared/components/ImageEditor/resize/resize.css +56 -0
  61. package/src/shared/components/ImageEditor/sidebar/Sidebar.tsx +54 -0
  62. package/src/shared/components/ImageEditor/sidebar/SidebarCrop.tsx +57 -0
  63. package/src/shared/components/ImageEditor/sidebar/SidebarPresets.tsx +54 -0
  64. package/src/shared/components/ImageEditor/sidebar/SidebarResize.tsx +26 -0
  65. package/src/shared/components/ImageEditor/sidebar/index.ts +1 -0
  66. package/src/shared/components/ImageEditor/sidebar/sidebar.css +101 -0
  67. package/src/shared/components/ImageEditor/types.ts +59 -0
  68. package/src/shared/components/ImageEditor/ui/button/Button.stories.tsx +42 -0
  69. package/src/shared/components/ImageEditor/ui/button/Button.tsx +21 -0
  70. package/src/shared/components/ImageEditor/ui/button/button.css +69 -0
  71. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.stories.tsx +26 -0
  72. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.tsx +29 -0
  73. package/src/shared/components/ImageEditor/ui/button-crop/button-crop.css +38 -0
  74. package/src/shared/components/ImageEditor/ui/index.ts +6 -0
  75. package/src/shared/components/ImageEditor/ui/input/Input.stories.tsx +45 -0
  76. package/src/shared/components/ImageEditor/ui/input/Input.tsx +20 -0
  77. package/src/shared/components/ImageEditor/ui/input/input.css +48 -0
  78. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.stories.tsx +33 -0
  79. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.tsx +23 -0
  80. package/src/shared/components/ImageEditor/ui/input-pixel/input-pixel.css +24 -0
  81. package/src/shared/components/ImageEditor/ui/select/Select.stories.tsx +67 -0
  82. package/src/shared/components/ImageEditor/ui/select/Select.tsx +161 -0
  83. package/src/shared/components/ImageEditor/ui/select/select.css +147 -0
  84. package/src/shared/components/ImageEditor/ui/separator/Separator.stories.tsx +37 -0
  85. package/src/shared/components/ImageEditor/ui/separator/Separator.tsx +27 -0
  86. package/src/shared/components/ImageEditor/ui/separator/separator.css +19 -0
  87. package/src/shared/components/ImageEditor/utils.test.ts +136 -0
  88. package/src/shared/components/ImageEditor/utils.ts +497 -0
  89. package/src/shared/components/ui/index.ts +1 -0
  90. package/src/shared/components/ui/preloader.tsx +78 -0
  91. package/src/shared/config/index.ts +3 -0
  92. package/src/shared/config/routes.ts +4 -0
  93. package/src/shared/features/gallery/GalleryPage.tsx +33 -0
  94. package/src/shared/features/gallery/ImageEditPage.tsx +37 -0
  95. package/src/shared/features/gallery/index.ts +2 -0
  96. package/src/shared/hooks/index.ts +1 -0
  97. package/src/shared/hooks/useImagePreload.tsx +49 -0
  98. package/src/shared/types/index.ts +11 -0
  99. package/src/test/setup.ts +14 -0
  100. package/src/virtual-css-injected-by-js.d.ts +8 -0
  101. package/src/widget.tsx +119 -0
  102. package/tsconfig.app.json +29 -0
  103. package/tsconfig.json +12 -0
  104. package/tsconfig.lib.json +18 -0
  105. package/tsconfig.node.json +23 -0
  106. package/vite-env.d.ts +1 -0
  107. package/vite.config.ts +24 -0
  108. package/vite.lib.config.ts +33 -0
  109. package/vite.widget.config.ts +39 -0
  110. package/wrangler.json +5 -0
@@ -0,0 +1,54 @@
1
+ // import { motion, type Variants } from "motion/react";
2
+ // import { Link } from "react-router";
3
+ // import type { Image } from "@/shared/types";
4
+
5
+ // type ImageCardProps = {
6
+ // image: Image;
7
+ // };
8
+
9
+ // export const ImageCard = ({ image }: ImageCardProps) => {
10
+ // const imageVariants: Variants = {
11
+ // rest: { scale: 1, opacity: 0.5 },
12
+ // hover: {
13
+ // scale: 1.1,
14
+ // opacity: 1,
15
+ // },
16
+ // };
17
+
18
+ // return (
19
+ // <Link to={`/images/${image.id}`}>
20
+ // <motion.div
21
+ // className={`
22
+ // relative
23
+ // w-full aspect-4/3
24
+ // overflow-hidden cursor-pointer box-border
25
+ // border border-neutral-400
26
+ // `}
27
+ // whileHover="hover"
28
+ // initial="rest"
29
+ // animate="rest"
30
+ // >
31
+ // <motion.div
32
+ // className="absolute inset-0 opacity-50"
33
+ // variants={imageVariants}
34
+ // transition={{ duration: 0.3, ease: "easeIn" }}
35
+ // style={{
36
+ // backgroundImage: `url(${image.thumbnail})`,
37
+ // backgroundSize: "cover",
38
+ // backgroundPosition: "center",
39
+ // }}
40
+ // />
41
+ // </motion.div>
42
+ // </Link>
43
+ // );
44
+ // };
45
+
46
+ type ImageCardProps = {
47
+ image: {
48
+ id: string;
49
+ };
50
+ };
51
+
52
+ export const ImageCard = ({ image }: ImageCardProps) => {
53
+ return <div>{image.id}</div>;
54
+ };
@@ -0,0 +1 @@
1
+ export { ImageCard } from "./ImageCard";
@@ -0,0 +1,48 @@
1
+ import { Preloader } from "@/shared/components/ui";
2
+ import { ImageEditorProvider } from "./provider/ImageEditorProvider";
3
+ import { useImageLoader } from "./hooks";
4
+ import { Header } from "./header";
5
+ import { Frame } from "./frame";
6
+ import { Sidebar } from "./sidebar";
7
+ import type { FuncSaveArgs } from "./types";
8
+ import "./index.css";
9
+
10
+ type ImageEditorProps = {
11
+ image: string;
12
+ onSave: FuncSaveArgs;
13
+ onBack: () => void;
14
+ };
15
+
16
+ export const ImageEditor = ({ image, onSave, onBack }: ImageEditorProps) => {
17
+ const { loading, error, width, height, base64, extension } =
18
+ useImageLoader(image);
19
+
20
+ if (loading || error || !base64 || !extension) {
21
+ return (
22
+ <div className="picedi sys">
23
+ {loading && <Preloader size={48} />}
24
+ {error && <div className="text-red">{error}</div>}
25
+ {!loading && !error && (!base64 || !extension) && (
26
+ <div className="text-red">Failed to load image.</div>
27
+ )}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <ImageEditorProvider
34
+ base64={base64}
35
+ width={width}
36
+ height={height}
37
+ ext={extension}
38
+ >
39
+ <div className="picedi main">
40
+ <Header onSave={onSave} onBack={onBack} />
41
+ <div className="grid">
42
+ <Frame />
43
+ <Sidebar />
44
+ </div>
45
+ </div>
46
+ </ImageEditorProvider>
47
+ );
48
+ };
@@ -0,0 +1,114 @@
1
+ import * as React from "react";
2
+
3
+ const baseSvgProps: React.ComponentPropsWithoutRef<"svg"> = {
4
+ xmlns: "http://w3.org",
5
+ width: "24",
6
+ height: "24",
7
+ viewBox: "0 0 24 24",
8
+ fill: "none",
9
+ stroke: "currentColor",
10
+ strokeWidth: "2",
11
+ strokeLinecap: "round",
12
+ strokeLinejoin: "round",
13
+ };
14
+
15
+ export const Check = (props: React.ComponentPropsWithoutRef<"svg">) => (
16
+ <svg {...baseSvgProps} {...props}>
17
+ <path d="M20 6 9 17l-5-5" />
18
+ </svg>
19
+ );
20
+
21
+ export const X = (props: React.ComponentPropsWithoutRef<"svg">) => (
22
+ <svg {...baseSvgProps} {...props}>
23
+ <path d="M18 6 6 18" />
24
+ <path d="m6 6 12 12" />
25
+ </svg>
26
+ );
27
+
28
+ export const Lock = (props: React.ComponentPropsWithoutRef<"svg">) => (
29
+ <svg {...baseSvgProps} {...props}>
30
+ <rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
31
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
32
+ </svg>
33
+ );
34
+
35
+ export const ArrowLeft = (props: React.ComponentPropsWithoutRef<"svg">) => (
36
+ <svg {...baseSvgProps} {...props}>
37
+ <path d="m12 19-7-7 7-7" />
38
+ <path d="M19 12H5" />
39
+ </svg>
40
+ );
41
+
42
+ export const ChevronDown = (props: React.ComponentPropsWithoutRef<"svg">) => (
43
+ <svg {...baseSvgProps} {...props}>
44
+ <path d="m6 9 6 6 6-6" />
45
+ </svg>
46
+ );
47
+
48
+ export const Undo = (props: React.ComponentPropsWithoutRef<"svg">) => (
49
+ <svg {...baseSvgProps} {...props}>
50
+ <path d="M3 7v6h6" />
51
+ <path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
52
+ </svg>
53
+ );
54
+
55
+ export const Redo = (props: React.ComponentPropsWithoutRef<"svg">) => (
56
+ <svg {...baseSvgProps} {...props}>
57
+ <path d="M21 7v6h-6" />
58
+ <path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7" />
59
+ </svg>
60
+ );
61
+
62
+ export const Settings = (props: React.ComponentPropsWithoutRef<"svg">) => (
63
+ <svg {...baseSvgProps} {...props}>
64
+ <path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
65
+ <circle cx="12" cy="12" r="3" />
66
+ </svg>
67
+ );
68
+
69
+ export const Crop = (props: React.ComponentPropsWithoutRef<"svg">) => (
70
+ <svg {...baseSvgProps} {...props}>
71
+ <path d="M6 2v14a2 2 0 0 0 2 2h14" />
72
+ <path d="M18 22V8a2 2 0 0 0-2-2H2" />
73
+ </svg>
74
+ );
75
+
76
+ export const Image = (props: React.ComponentPropsWithoutRef<"svg">) => (
77
+ <svg {...baseSvgProps} {...props}>
78
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
79
+ <circle cx="9" cy="9" r="2" />
80
+ <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
81
+ </svg>
82
+ );
83
+
84
+ export const Square = (props: React.ComponentPropsWithoutRef<"svg">) => (
85
+ <svg {...baseSvgProps} {...props}>
86
+ <rect width="18" height="18" x="3" y="3" rx="2" />
87
+ </svg>
88
+ );
89
+
90
+ export const Fullscreen = (props: React.ComponentPropsWithoutRef<"svg">) => (
91
+ <svg {...baseSvgProps} {...props}>
92
+ <path d="M3 7V5a2 2 0 0 1 2-2h2" />
93
+ <path d="M17 3h2a2 2 0 0 1 2 2v2" />
94
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2" />
95
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2" />
96
+ <rect width="10" height="8" x="7" y="8" rx="1" />
97
+ </svg>
98
+ );
99
+
100
+ export const Loader = (props: React.ComponentPropsWithoutRef<"svg">) => (
101
+ <svg {...baseSvgProps} {...props}>
102
+ <path d="M21 12a9 9 0 1 1-6.219-8.56">
103
+ <animateTransform
104
+ attributeType="xml"
105
+ attributeName="transform"
106
+ type="rotate"
107
+ from="0 12 12"
108
+ to="360 12 12"
109
+ dur="0.8s"
110
+ repeatCount="indefinite"
111
+ />
112
+ </path>
113
+ </svg>
114
+ );
@@ -0,0 +1,118 @@
1
+ export type PresetOptions = {
2
+ value: string;
3
+ label: string;
4
+ w: number;
5
+ h: number;
6
+ rightLabel?: string;
7
+ };
8
+
9
+ export type Preset = {
10
+ value: string;
11
+ label: string;
12
+ options: Array<PresetOptions>;
13
+ };
14
+
15
+ export const presetsData: Array<Preset> = [
16
+ {
17
+ label: "Facebook",
18
+ value: "facebook",
19
+ options: [
20
+ {
21
+ value: "facebook-post",
22
+ label: "Post",
23
+ w: 1200,
24
+ h: 630,
25
+ rightLabel: "1200 x 630",
26
+ },
27
+ {
28
+ value: "facebook-cover",
29
+ label: "Cover",
30
+ w: 851,
31
+ h: 315,
32
+ rightLabel: "851 x 315",
33
+ },
34
+ {
35
+ value: "facebook-profile",
36
+ label: "Profile",
37
+ w: 170,
38
+ h: 170,
39
+ rightLabel: "170 x 170",
40
+ },
41
+ {
42
+ value: "facebook-story",
43
+ label: "Story",
44
+ w: 1080,
45
+ h: 1920,
46
+ rightLabel: "1080 x 1920",
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ label: "Instagram",
52
+ value: "instagram",
53
+ options: [
54
+ {
55
+ value: "instagram-landscape",
56
+ label: "Landscape",
57
+ w: 1080,
58
+ h: 566,
59
+ rightLabel: "1080 x 566",
60
+ },
61
+ {
62
+ value: "instagram-portait",
63
+ label: "Portait",
64
+ w: 1080,
65
+ h: 1350,
66
+ rightLabel: "1080 x 1350",
67
+ },
68
+ {
69
+ value: "instagram-square",
70
+ label: "Square",
71
+ w: 1080,
72
+ h: 1080,
73
+ rightLabel: "1080 x 1080",
74
+ },
75
+ {
76
+ value: "instagram-story",
77
+ label: "Story",
78
+ w: 1080,
79
+ h: 1920,
80
+ rightLabel: "1080 x 1920",
81
+ },
82
+ {
83
+ value: "instagram-thumbnail",
84
+ label: "Thumbnail",
85
+ w: 161,
86
+ h: 161,
87
+ rightLabel: "161 x 161",
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ label: "LinkedIn",
93
+ value: "linkedin",
94
+ options: [
95
+ {
96
+ value: "linkedin-blog-post",
97
+ label: "Blog Post",
98
+ w: 1200,
99
+ h: 627,
100
+ rightLabel: "1200 x 627",
101
+ },
102
+ {
103
+ value: "linkedin-cover",
104
+ label: "Cover",
105
+ w: 1128,
106
+ h: 191,
107
+ rightLabel: "1128 x 191",
108
+ },
109
+ {
110
+ value: "linkedin-profile",
111
+ label: "Profile",
112
+ w: 400,
113
+ h: 400,
114
+ rightLabel: "400 x 400",
115
+ },
116
+ ],
117
+ },
118
+ ];
@@ -0,0 +1,60 @@
1
+ import { useRef } from "react";
2
+ import { useImageEditorContext } from "../provider/useImageEditorContext";
3
+ import {
4
+ CropBorders,
5
+ useCropInteraction,
6
+ CropPointers,
7
+ CropLines,
8
+ CropPointer,
9
+ } from "../crop";
10
+ import { getInitalCrop } from "../utils";
11
+ import { useMobile } from "../hooks";
12
+ import "./crop.css";
13
+
14
+ export const Crop = () => {
15
+ const { getCurrentAction, getLastHistoryItem } = useImageEditorContext();
16
+ const currentAction = getCurrentAction();
17
+ const mobile = useMobile();
18
+
19
+ const imgRef = useRef<HTMLImageElement>(null);
20
+ const cropRef = useRef<HTMLDivElement>(null);
21
+
22
+ const { handleCropStart } = useCropInteraction({
23
+ imgRef,
24
+ cropRef,
25
+ });
26
+
27
+ if (!currentAction || currentAction.name !== "crop") {
28
+ return null;
29
+ }
30
+
31
+ const { width, height, base64 } = getLastHistoryItem();
32
+ const { x, y, w, h } = getInitalCrop(currentAction.args.ratio, width, height);
33
+
34
+ return (
35
+ <>
36
+ <img
37
+ ref={imgRef}
38
+ src={base64}
39
+ alt="Image"
40
+ className="crop-image"
41
+ style={{ clipPath: `xywh(${x}% ${y}% ${w}% ${h}%)` }}
42
+ />
43
+ <div
44
+ ref={cropRef}
45
+ className="crop-box"
46
+ style={{ width: `${w}%`, height: `${h}%`, top: `${y}%`, left: `${x}%` }}
47
+ >
48
+ <CropLines />
49
+ {mobile ? (
50
+ <CropPointer onMouseDown={handleCropStart} />
51
+ ) : (
52
+ <>
53
+ <CropBorders onMouseDown={handleCropStart} />
54
+ <CropPointers onMouseDown={handleCropStart} />
55
+ </>
56
+ )}
57
+ </div>
58
+ </>
59
+ );
60
+ };
@@ -0,0 +1,28 @@
1
+ import type { Direction } from "../types";
2
+
3
+ type CropBordersProps = {
4
+ onMouseDown: (e: React.MouseEvent, type: Direction, cursor: string) => void;
5
+ };
6
+
7
+ export const CropBorders = ({ onMouseDown }: CropBordersProps) => {
8
+ return (
9
+ <>
10
+ <span
11
+ className="crop-b crop-b-t"
12
+ onMouseDown={(e: React.MouseEvent) => onMouseDown(e, "t", "ns")}
13
+ />
14
+ <span
15
+ className="crop-b crop-b-r"
16
+ onMouseDown={(e) => onMouseDown(e, "r", "ew")}
17
+ />
18
+ <span
19
+ className="crop-b crop-b-b"
20
+ onMouseDown={(e) => onMouseDown(e, "b", "ns")}
21
+ />
22
+ <span
23
+ className="crop-b crop-b-l"
24
+ onMouseDown={(e) => onMouseDown(e, "l", "ew")}
25
+ />
26
+ </>
27
+ );
28
+ };
@@ -0,0 +1,8 @@
1
+ export const CropLines = () => {
2
+ return (
3
+ <div className="crop-l-box">
4
+ <span className="crop-l crop-l-v" />
5
+ <span className="crop-l crop-l-h" />
6
+ </div>
7
+ );
8
+ };
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import type { Direction } from "../types";
3
+
4
+ type CropPointerProps = {
5
+ onMouseDown: (
6
+ e: React.MouseEvent | React.TouchEvent,
7
+ type: Direction,
8
+ cursor?: string,
9
+ ) => void;
10
+ };
11
+
12
+ export const CropPointer = ({ onMouseDown }: CropPointerProps) => {
13
+ return (
14
+ <div className="crop-p-b">
15
+ <div
16
+ className="crop-p-p"
17
+ onTouchStart={(e) => onMouseDown(e, "br", "nwse")}
18
+ />
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,44 @@
1
+ import type { Direction } from "../types";
2
+
3
+ type CropPointersProps = {
4
+ onMouseDown: (e: React.MouseEvent, type: Direction, cursor: string) => void;
5
+ };
6
+
7
+ export const CropPointers = ({ onMouseDown }: CropPointersProps) => {
8
+ return (
9
+ <>
10
+ <span
11
+ className="crop-ps crop-ps-tl"
12
+ onMouseDown={(e) => onMouseDown(e, "tl", "nwse")}
13
+ />
14
+ <span
15
+ className="crop-ps crop-ps-t"
16
+ onMouseDown={(e) => onMouseDown(e, "t", "ns")}
17
+ />
18
+ <span
19
+ className="crop-ps crop-ps-tr"
20
+ onMouseDown={(e) => onMouseDown(e, "tr", "nesw")}
21
+ />
22
+ <span
23
+ className="crop-ps crop-ps-r"
24
+ onMouseDown={(e) => onMouseDown(e, "r", "ew")}
25
+ />
26
+ <span
27
+ className="crop-ps crop-ps-br"
28
+ onMouseDown={(e) => onMouseDown(e, "br", "nwse")}
29
+ />
30
+ <span
31
+ className="crop-ps crop-ps-b"
32
+ onMouseDown={(e) => onMouseDown(e, "b", "ns")}
33
+ />
34
+ <span
35
+ className="crop-ps crop-ps-bl"
36
+ onMouseDown={(e) => onMouseDown(e, "bl", "nesw")}
37
+ />
38
+ <span
39
+ className="crop-ps crop-ps-l"
40
+ onMouseDown={(e) => onMouseDown(e, "l", "ew")}
41
+ />
42
+ </>
43
+ );
44
+ };
@@ -0,0 +1,138 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Button, Separator } from "../ui";
3
+ import type { CropRect } from "../types";
4
+ import { useImageEditorContext } from "../provider/useImageEditorContext";
5
+ import { eventBus } from "../eventBus";
6
+ import { getInitalCrop } from "../utils";
7
+ import { useImageProcessor } from "../hooks";
8
+ import { Check, Loader, X } from "../assets/icons";
9
+
10
+ export const CropTools = () => {
11
+ const {
12
+ setCurrentAction,
13
+ getCurrentAction,
14
+ getLastHistoryItem,
15
+ addToHistory,
16
+ setSidebar,
17
+ } = useImageEditorContext();
18
+ const { crop, resize } = useImageProcessor();
19
+ const [loading, setLoading] = useState(false);
20
+ const action = getCurrentAction();
21
+ const { base64, width, height } = getLastHistoryItem();
22
+
23
+ const leftRef = useRef<HTMLDivElement>(null);
24
+ const topRef = useRef<HTMLDivElement>(null);
25
+ const widthRef = useRef<HTMLDivElement>(null);
26
+ const heightRef = useRef<HTMLDivElement>(null);
27
+ const cropRectRef = useRef<CropRect | null>(null);
28
+
29
+ const handleClose = () => {
30
+ setCurrentAction(null);
31
+ setSidebar(true);
32
+ };
33
+
34
+ useEffect(() => {
35
+ if (action?.name !== "crop") {
36
+ return;
37
+ }
38
+ const { x, y, w, h } = getInitalCrop(action.args.ratio, width, height);
39
+ const xPx = Math.round((x / 100) * width);
40
+ const yPx = Math.round((y / 100) * height);
41
+ const wPx = Math.round((w / 100) * width);
42
+ const hPx = Math.round((h / 100) * height);
43
+
44
+ if (leftRef.current) leftRef.current.textContent = xPx.toString();
45
+ if (topRef.current) topRef.current.textContent = yPx.toString();
46
+ if (widthRef.current) widthRef.current.textContent = wPx.toString();
47
+ if (heightRef.current) heightRef.current.textContent = hPx.toString();
48
+ cropRectRef.current = { x: xPx, y: yPx, w: wPx, h: hPx };
49
+
50
+ const onCropUpdate = (event: Event) => {
51
+ const customEvent = event as CustomEvent<CropRect>;
52
+ const { x, y, w, h } = customEvent.detail;
53
+ cropRectRef.current = customEvent.detail;
54
+ if (leftRef.current) leftRef.current.textContent = x.toString();
55
+ if (topRef.current) topRef.current.textContent = y.toString();
56
+ if (widthRef.current) widthRef.current.textContent = w.toString();
57
+ if (heightRef.current) heightRef.current.textContent = h.toString();
58
+ };
59
+
60
+ eventBus.addEventListener("crop-update", onCropUpdate);
61
+
62
+ return () => {
63
+ eventBus.removeEventListener("crop-update", onCropUpdate);
64
+ };
65
+ }, [action, width, height]);
66
+
67
+ const handleSave = async () => {
68
+ if (!action || action.name !== "crop") {
69
+ return;
70
+ }
71
+ try {
72
+ setLoading(true);
73
+ if (!cropRectRef.current) {
74
+ return;
75
+ }
76
+ const { x, y, w, h } = cropRectRef.current;
77
+ const { preset } = action.args;
78
+ let processedBase64 = await crop(base64, x, y, w, h);
79
+
80
+ if (preset) {
81
+ const { w, h } = preset;
82
+ processedBase64 = await resize(processedBase64, w, h);
83
+ }
84
+
85
+ addToHistory({
86
+ name: "Crop",
87
+ base64: processedBase64,
88
+ width: preset ? preset.w : cropRectRef.current!.w,
89
+ height: preset ? preset.h : cropRectRef.current!.h,
90
+ });
91
+ setSidebar(true);
92
+ } catch (error) {
93
+ console.error("Failed to process image:", error);
94
+ } finally {
95
+ setLoading(false);
96
+ }
97
+ };
98
+
99
+ return (
100
+ <div className="crop-tools">
101
+ <div className="crop-tools-info">
102
+ <div className="crop-tools-info-label">left</div>
103
+ <div className="crop-tools-info-value semibold" ref={leftRef} />
104
+ </div>
105
+ <div className="crop-tools-info">
106
+ <div className="crop-tools-info-label">top</div>
107
+ <div className="crop-tools-info-value semibold" ref={topRef} />
108
+ </div>
109
+ <div className="crop-tools-info">
110
+ <div className="crop-tools-info-label">width</div>
111
+ <div className="crop-tools-info-value semibold" ref={widthRef} />
112
+ </div>
113
+ <div className="crop-tools-info">
114
+ <div className="crop-tools-info-label">height</div>
115
+ <div className="crop-tools-info-value semibold" ref={heightRef} />
116
+ </div>
117
+ <Separator orientation="vertical" />
118
+ <Button
119
+ variant="ghost"
120
+ className="text-green"
121
+ style={{ height: "36px", width: "36px" }}
122
+ disabled={loading}
123
+ onClick={handleSave}
124
+ >
125
+ {loading ? <Loader /> : <Check />}
126
+ </Button>
127
+ <Separator orientation="vertical" />
128
+ <Button
129
+ variant="ghost"
130
+ style={{ height: "36px", width: "36px" }}
131
+ disabled={loading}
132
+ onClick={handleClose}
133
+ >
134
+ <X className="text-red" />
135
+ </Button>
136
+ </div>
137
+ );
138
+ };