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,126 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import type { PropsWithChildren } from "react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { ImageEditorProvider } from "./ImageEditorProvider";
5
+ import { useImageEditorContext } from "./useImageEditorContext";
6
+
7
+ const initialItem = {
8
+ name: "Initial",
9
+ base64: "data:image/png;base64,initial",
10
+ width: 800,
11
+ height: 600,
12
+ };
13
+
14
+ const editedItem = {
15
+ name: "Resize",
16
+ base64: "data:image/png;base64,edited",
17
+ width: 400,
18
+ height: 300,
19
+ };
20
+
21
+ const secondEditedItem = {
22
+ name: "Crop",
23
+ base64: "data:image/png;base64,cropped",
24
+ width: 200,
25
+ height: 200,
26
+ };
27
+
28
+ const wrapper = ({ children }: PropsWithChildren) => (
29
+ <ImageEditorProvider
30
+ base64={initialItem.base64}
31
+ width={initialItem.width}
32
+ height={initialItem.height}
33
+ ext="png"
34
+ >
35
+ {children}
36
+ </ImageEditorProvider>
37
+ );
38
+
39
+ describe("ImageEditorProvider", () => {
40
+ it("initializes the image history and editor state", () => {
41
+ const { result } = renderHook(() => useImageEditorContext(), { wrapper });
42
+
43
+ expect(result.current.history).toEqual({ items: [initialItem], pointer: 0 });
44
+ expect(result.current.extension).toBe("png");
45
+ expect(result.current.currentAction).toBeNull();
46
+ expect(result.current.getSidebar()).toBe(false);
47
+ expect(result.current.getLastHistoryItem()).toEqual(initialItem);
48
+ });
49
+
50
+ it("updates the selected action and sidebar visibility", () => {
51
+ const { result } = renderHook(() => useImageEditorContext(), { wrapper });
52
+ const action = {
53
+ name: "resize" as const,
54
+ args: { width: 400, height: 300 },
55
+ };
56
+
57
+ act(() => {
58
+ result.current.setCurrentAction(action);
59
+ result.current.setSidebar(true);
60
+ });
61
+
62
+ expect(result.current.getCurrentAction()).toEqual(action);
63
+ expect(result.current.getSidebar()).toBe(true);
64
+ });
65
+
66
+ it("adds edits, clears the action, and discards redo history", () => {
67
+ const { result } = renderHook(() => useImageEditorContext(), { wrapper });
68
+
69
+ act(() => {
70
+ result.current.setCurrentAction({
71
+ name: "resize",
72
+ args: { width: 400, height: 300 },
73
+ });
74
+ result.current.addToHistory(editedItem);
75
+ result.current.addToHistory(secondEditedItem);
76
+ result.current.undo();
77
+ result.current.addToHistory({ ...editedItem, name: "Replacement" });
78
+ });
79
+
80
+ expect(result.current.currentAction).toBeNull();
81
+ expect(result.current.history.pointer).toBe(2);
82
+ expect(result.current.history.items).toEqual([
83
+ initialItem,
84
+ editedItem,
85
+ { ...editedItem, name: "Replacement" },
86
+ ]);
87
+ });
88
+
89
+ it("moves through history without exceeding its boundaries", () => {
90
+ const { result } = renderHook(() => useImageEditorContext(), { wrapper });
91
+
92
+ act(() => {
93
+ result.current.undo();
94
+ });
95
+ expect(result.current.history.pointer).toBe(0);
96
+
97
+ act(() => {
98
+ result.current.addToHistory(editedItem);
99
+ result.current.undo();
100
+ });
101
+ expect(result.current.history.pointer).toBe(0);
102
+ expect(result.current.getLastHistoryItem()).toEqual(initialItem);
103
+
104
+ act(() => {
105
+ result.current.redo();
106
+ result.current.redo();
107
+ });
108
+ expect(result.current.history.pointer).toBe(1);
109
+ expect(result.current.getLastHistoryItem()).toEqual(editedItem);
110
+ });
111
+
112
+ it("resets to the source image and resets history after saving", () => {
113
+ const { result } = renderHook(() => useImageEditorContext(), { wrapper });
114
+
115
+ act(() => {
116
+ result.current.addToHistory(editedItem);
117
+ result.current.resetHistoryAfterSave();
118
+ });
119
+ expect(result.current.history).toEqual({ items: [editedItem], pointer: 0 });
120
+
121
+ act(() => {
122
+ result.current.resetHistory();
123
+ });
124
+ expect(result.current.history).toEqual({ items: [initialItem], pointer: 0 });
125
+ });
126
+ });
@@ -0,0 +1,107 @@
1
+ import { useState } from "react";
2
+ import { getInitialState } from "./initialState";
3
+ import { StoreContext } from "./StoreContext";
4
+ import type { ImageEditorContextType } from "./initialState";
5
+ import type { Action, HistoryItem } from "../types";
6
+
7
+ type ImageEditorProviderProps = {
8
+ children: React.ReactNode;
9
+ base64: string;
10
+ width: number;
11
+ height: number;
12
+ ext: string;
13
+ };
14
+
15
+ export const ImageEditorProvider = ({
16
+ children,
17
+ base64,
18
+ width,
19
+ height,
20
+ ext,
21
+ }: ImageEditorProviderProps) => {
22
+ const [state, setState] = useState<ImageEditorContextType>(
23
+ getInitialState(ext, width, height, base64),
24
+ );
25
+
26
+ const getCurrentAction = () => state.currentAction;
27
+
28
+ const setCurrentAction = (payload: Action | null) =>
29
+ setState((store) => ({ ...store, currentAction: payload }));
30
+
31
+ const addToHistory = (payload: HistoryItem) =>
32
+ setState((store) => {
33
+ const incrementedPointer = store.history.pointer + 1;
34
+ const updatedItems = [
35
+ ...store.history.items.slice(0, incrementedPointer),
36
+ payload,
37
+ ];
38
+ return {
39
+ ...store,
40
+ currentAction: null,
41
+ history: {
42
+ items: updatedItems,
43
+ pointer: incrementedPointer,
44
+ },
45
+ };
46
+ });
47
+
48
+ const getLastHistoryItem = () =>
49
+ state.history.items.at(state.history.pointer)!;
50
+
51
+ const resetHistory = () =>
52
+ setState((store) => ({
53
+ ...store,
54
+ history: getInitialState(ext, width, height, base64).history,
55
+ }));
56
+
57
+ const resetHistoryAfterSave = () =>
58
+ setState((store) => {
59
+ const savedHistoryItem = store.history.items.at(store.history.pointer)!;
60
+ return {
61
+ ...store,
62
+ history: {
63
+ items: [savedHistoryItem],
64
+ pointer: 0,
65
+ },
66
+ };
67
+ });
68
+
69
+ const undo = () =>
70
+ setState((store) => {
71
+ if (store.history.pointer <= 0) return store;
72
+ return {
73
+ ...store,
74
+ history: { ...store.history, pointer: store.history.pointer - 1 },
75
+ };
76
+ });
77
+
78
+ const redo = () =>
79
+ setState((store) => {
80
+ if (store.history.pointer >= store.history.items.length - 1) return store;
81
+ return {
82
+ ...store,
83
+ history: { ...store.history, pointer: store.history.pointer + 1 },
84
+ };
85
+ });
86
+
87
+ const getSidebar = () => state.sidebar;
88
+
89
+ const setSidebar = (payload: boolean) =>
90
+ setState((store) => ({ ...store, sidebar: payload }));
91
+
92
+ const value = {
93
+ ...state,
94
+ getCurrentAction,
95
+ setCurrentAction,
96
+ addToHistory,
97
+ resetHistory,
98
+ resetHistoryAfterSave,
99
+ undo,
100
+ redo,
101
+ getLastHistoryItem,
102
+ getSidebar,
103
+ setSidebar,
104
+ };
105
+
106
+ return <StoreContext value={value}>{children}</StoreContext>;
107
+ };
@@ -0,0 +1,20 @@
1
+ import { createContext } from "react";
2
+ import { type ImageEditorContextType } from "./initialState";
3
+ import type { Action, HistoryItem } from "../types";
4
+
5
+ type StoreContextType = ImageEditorContextType & {
6
+ getCurrentAction: () => Action | null;
7
+ setCurrentAction: (payload: Action | null) => void;
8
+ addToHistory: (payload: HistoryItem) => void;
9
+ getLastHistoryItem: () => HistoryItem;
10
+ resetHistory: () => void;
11
+ resetHistoryAfterSave: () => void;
12
+ undo: () => void;
13
+ redo: () => void;
14
+ getSidebar: () => boolean;
15
+ setSidebar: (payload: boolean) => void;
16
+ };
17
+
18
+ export const StoreContext = createContext<StoreContextType | undefined>(
19
+ undefined,
20
+ );
@@ -0,0 +1,30 @@
1
+ import type { History, Action } from "../types";
2
+
3
+ export type ImageEditorContextType = {
4
+ history: History;
5
+ currentAction: Action | null;
6
+ extension: string | null;
7
+ sidebar: boolean;
8
+ };
9
+
10
+ export const getInitialState = (
11
+ extension: string,
12
+ width: number,
13
+ height: number,
14
+ base64: string,
15
+ ): ImageEditorContextType => ({
16
+ history: {
17
+ pointer: 0,
18
+ items: [
19
+ {
20
+ name: "Initial",
21
+ base64,
22
+ width,
23
+ height,
24
+ },
25
+ ],
26
+ },
27
+ currentAction: null,
28
+ extension,
29
+ sidebar: false,
30
+ });
@@ -0,0 +1,12 @@
1
+ import { useContext } from "react";
2
+ import { StoreContext } from "./StoreContext";
3
+
4
+ export const useImageEditorContext = () => {
5
+ const context = useContext(StoreContext);
6
+ if (!context) {
7
+ throw new Error(
8
+ "useImageEditorContext must be used within an ImageEditorProvider"
9
+ );
10
+ }
11
+ return context;
12
+ };
@@ -0,0 +1,183 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useImageEditorContext } from "../provider/useImageEditorContext";
3
+ import { Loader, Check, X, Lock } from "../assets/icons";
4
+ import { useImageProcessor, useMobile } from "../hooks";
5
+ import { Button } from "../ui";
6
+ import { InputPixel } from "../ui";
7
+ import "./resize.css";
8
+
9
+ const minScale = 10;
10
+ const maxScale = 200;
11
+
12
+ const sizeCalculator = (scale: number, width: number, height: number) => {
13
+ if (scale < minScale) scale = minScale;
14
+ if (scale > maxScale) scale = maxScale;
15
+ return {
16
+ updatedScale: scale,
17
+ updatedWidth: Math.round(width * (scale / 100)),
18
+ updatedHeight: Math.round(height * (scale / 100)),
19
+ };
20
+ };
21
+
22
+ type ResizeToolsProps = {
23
+ onResizing: (scale: number) => void;
24
+ };
25
+
26
+ export const ResizeTools = ({ onResizing }: ResizeToolsProps) => {
27
+ const { getLastHistoryItem, setCurrentAction, addToHistory, setSidebar } =
28
+ useImageEditorContext();
29
+ const {
30
+ width: currentWidth,
31
+ height: currentHeight,
32
+ base64,
33
+ } = getLastHistoryItem();
34
+ const [loading, setLoading] = useState(false);
35
+ const [width, setWidth] = useState(currentWidth);
36
+ const [height, setHeight] = useState(currentHeight);
37
+ const startVerticalSlideRef = useRef<number>(0);
38
+ const { resize } = useImageProcessor();
39
+ const mobile = useMobile();
40
+
41
+ const handleChangeWidth = (e: React.ChangeEvent<HTMLInputElement>) => {
42
+ const { updatedScale, updatedWidth, updatedHeight } = sizeCalculator(
43
+ (parseInt(e.target.value) / currentWidth) * 100,
44
+ currentWidth,
45
+ currentHeight,
46
+ );
47
+ setWidth(updatedWidth);
48
+ setHeight(updatedHeight);
49
+ onResizing(updatedScale);
50
+ };
51
+
52
+ const handleChangeHeight = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ const { updatedScale, updatedWidth, updatedHeight } = sizeCalculator(
54
+ (parseInt(e.target.value) / currentHeight) * 100,
55
+ currentWidth,
56
+ currentHeight,
57
+ );
58
+ setHeight(updatedHeight);
59
+ setWidth(updatedWidth);
60
+ onResizing(updatedScale);
61
+ };
62
+
63
+ const handleClose = () => {
64
+ onResizing(100);
65
+ setCurrentAction(null);
66
+ setSidebar(true);
67
+ };
68
+
69
+ const handleSave = async () => {
70
+ try {
71
+ setLoading(true);
72
+ const processedBase64 = await resize(base64, width, height);
73
+ addToHistory({
74
+ name: "Resize",
75
+ base64: processedBase64,
76
+ width,
77
+ height,
78
+ });
79
+ onResizing(100);
80
+ setSidebar(true);
81
+ } catch (error) {
82
+ console.error("Failed to process image:", error);
83
+ } finally {
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ useEffect(() => {
89
+ let updatedScale = Math.round((width / currentWidth) * 100);
90
+
91
+ const updateScale = (scale: number) => {
92
+ const { updatedHeight, updatedWidth } = sizeCalculator(
93
+ scale,
94
+ currentWidth,
95
+ currentHeight,
96
+ );
97
+ setHeight(updatedHeight);
98
+ setWidth(updatedWidth);
99
+ onResizing(scale);
100
+ };
101
+
102
+ const handleWheel = (e: WheelEvent) => {
103
+ if (loading) return;
104
+ const direction = Math.sign(e.deltaY);
105
+ updatedScale -= direction * 2;
106
+ if (updatedScale <= minScale) updatedScale = minScale;
107
+ if (updatedScale >= maxScale) updatedScale = maxScale;
108
+ updateScale(updatedScale);
109
+ };
110
+
111
+ const handleTouchStart = (e: TouchEvent) => {
112
+ startVerticalSlideRef.current = e.touches[0].clientY;
113
+ };
114
+
115
+ const handleTouchMove = (e: TouchEvent) => {
116
+ if (loading) return;
117
+ const y = e.touches[0].clientY;
118
+ const deltaY = startVerticalSlideRef.current - y;
119
+ if (Math.abs(deltaY) < 10) return;
120
+ const direction = Math.sign(deltaY);
121
+ updatedScale += direction * 2;
122
+ if (updatedScale <= minScale) updatedScale = minScale;
123
+ if (updatedScale >= maxScale) updatedScale = maxScale;
124
+ updateScale(updatedScale);
125
+ startVerticalSlideRef.current = y;
126
+ };
127
+
128
+ const controller = new AbortController();
129
+ const { signal } = controller;
130
+
131
+ window.addEventListener("wheel", handleWheel, { signal });
132
+ window.addEventListener("touchstart", handleTouchStart, { signal });
133
+ window.addEventListener("touchmove", handleTouchMove, { signal });
134
+
135
+ return () => controller.abort();
136
+ }, [currentHeight, currentWidth, loading, mobile, onResizing, width]);
137
+
138
+ return (
139
+ <div className="resize">
140
+ <div className="resize-text">
141
+ {mobile ? "Slide UP/Down to Resize" : "Scroll UP/Down to Resize"}
142
+ </div>
143
+ <div className="resize-border" />
144
+ <div className="resize-tools">
145
+ <InputPixel
146
+ value={width}
147
+ name="width"
148
+ label="Width"
149
+ disabled={loading}
150
+ style={{ width: "88px" }}
151
+ onChange={handleChangeWidth}
152
+ />
153
+ <Lock className="resize-tools-lock" />
154
+ <InputPixel
155
+ value={height}
156
+ name="height"
157
+ label="Height"
158
+ disabled={loading}
159
+ style={{ width: "88px" }}
160
+ onChange={handleChangeHeight}
161
+ />
162
+ <div className="resize-tools-buttons">
163
+ <Button
164
+ variant="outline"
165
+ className="resize-tools-save text-green"
166
+ disabled={width === currentWidth || loading}
167
+ onClick={() => handleSave()}
168
+ >
169
+ {loading ? <Loader /> : <Check />}
170
+ </Button>
171
+ <Button
172
+ variant="outline"
173
+ className="resize-tools-close text-red"
174
+ disabled={loading}
175
+ onClick={handleClose}
176
+ >
177
+ <X />
178
+ </Button>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ );
183
+ };
@@ -0,0 +1 @@
1
+ export { ResizeTools } from "./ResizeTools";
@@ -0,0 +1,56 @@
1
+ .resize {
2
+ position: absolute;
3
+ display: flex;
4
+ flex-direction: column;
5
+ bottom: 24px;
6
+ left: 50%;
7
+ transform: translateX(-50%);
8
+ background-color: var(--background);
9
+ padding: 16px;
10
+ border-radius: 4px;
11
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
12
+ }
13
+
14
+ .resize-text {
15
+ font-size: 14px;
16
+ text-align: center;
17
+ color: var(--muted-foreground);
18
+ }
19
+
20
+ .resize-border {
21
+ margin: 12px auto 4px auto;
22
+ display: inline-block;
23
+ width: 60px;
24
+ border-bottom: 1px solid var(--border);
25
+ }
26
+
27
+ .resize-tools {
28
+ display: flex;
29
+ gap: 6px;
30
+ align-items: center;
31
+ margin-top: 16px;
32
+ }
33
+
34
+ .resize-tools-lock {
35
+ width: 12px;
36
+ height: 12px;
37
+ color: var(--muted-foreground);
38
+ }
39
+
40
+ .resize-tools-buttons {
41
+ display: flex;
42
+ margin-left: 4px;
43
+ }
44
+
45
+ .resize-tools-save {
46
+ width: 36px;
47
+ border-bottom-right-radius: 0;
48
+ border-top-right-radius: 0;
49
+ }
50
+
51
+ .resize-tools-close {
52
+ width: 36px;
53
+ border-bottom-left-radius: 0;
54
+ border-top-left-radius: 0;
55
+ margin-left: -1px;
56
+ }
@@ -0,0 +1,54 @@
1
+ import { useBellow } from "../hooks";
2
+ import { Settings, X } from "../assets/icons";
3
+ import { Button, Separator } from "../ui";
4
+ import { SidebarResize } from "./SidebarResize";
5
+ import { SidebarCrop } from "./SidebarCrop";
6
+ import { SidebarPresets } from "./SidebarPresets";
7
+ import { useImageEditorContext } from "../provider/useImageEditorContext";
8
+ import "./sidebar.css";
9
+
10
+ export const Sidebar = () => {
11
+ const { getSidebar, setSidebar } = useImageEditorContext();
12
+ const isSidebarOpen = getSidebar();
13
+ const isBellowMd = useBellow("md");
14
+
15
+ return (
16
+ <>
17
+ <Button
18
+ variant="outline"
19
+ className="sidebar-settings"
20
+ onClick={() => setSidebar(true)}
21
+ >
22
+ <Settings />
23
+ </Button>
24
+ <div
25
+ className="sidebar"
26
+ style={{
27
+ transform:
28
+ isBellowMd && isSidebarOpen
29
+ ? "translateX(-320px)"
30
+ : "translateX(0%)",
31
+ }}
32
+ >
33
+ <div className="sidebar-container">
34
+ <div className="sidebar-content">
35
+ {isBellowMd && (
36
+ <Button
37
+ variant="ghost"
38
+ className="sidebar-close-btn btn-rect"
39
+ onClick={() => setSidebar(false)}
40
+ >
41
+ <X />
42
+ </Button>
43
+ )}
44
+ <SidebarResize />
45
+ <Separator className="sidebar-separator" />
46
+ <SidebarCrop />
47
+ <Separator className="sidebar-separator" />
48
+ <SidebarPresets />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </>
53
+ );
54
+ };
@@ -0,0 +1,57 @@
1
+ import { Crop, Image, Square } from "../assets/icons";
2
+ import { useImageEditorContext } from "../provider/useImageEditorContext";
3
+ import type { ActionCrop } from "../types";
4
+ import { ButtonCrop } from "../ui";
5
+
6
+ export const SidebarCrop = () => {
7
+ const { currentAction, setCurrentAction, getLastHistoryItem, setSidebar } =
8
+ useImageEditorContext();
9
+ const { name, args } = currentAction || {};
10
+ const currentValue = name === "crop" ? (args?.id as string) : "";
11
+ const { width, height } = getLastHistoryItem();
12
+ const originRatio = width / height;
13
+
14
+ const handleClick = (value: string) => {
15
+ if (value === currentValue) {
16
+ return;
17
+ }
18
+
19
+ const ratio = value === "1:1" ? 1 : originRatio;
20
+
21
+ const args: ActionCrop = {
22
+ id: value,
23
+ ratio,
24
+ isFree: value === "freeform",
25
+ preset: null,
26
+ };
27
+
28
+ setCurrentAction({ name: "crop", args });
29
+ setSidebar(false);
30
+ };
31
+
32
+ return (
33
+ <div className="sidebar-crop">
34
+ <h4>Crop</h4>
35
+ <div className="sidebar-crop-ratios">
36
+ <ButtonCrop
37
+ label="Freeform"
38
+ icon={<Crop />}
39
+ active={currentValue === "freeform"}
40
+ onClick={() => handleClick("freeform")}
41
+ />
42
+ <ButtonCrop
43
+ label="Original"
44
+ icon={<Image />}
45
+ active={currentValue === "original"}
46
+ onClick={() => handleClick("original")}
47
+ />
48
+ <ButtonCrop
49
+ label="1:1"
50
+ icon={<Square />}
51
+ active={currentValue === "1:1"}
52
+ onClick={() => handleClick("1:1")}
53
+ />
54
+ </div>
55
+ </div>
56
+ );
57
+ };