@merchi/product-editor 0.1.9

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 (123) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/README.md +174 -0
  3. package/dist/components/AlignMenu.d.ts +3 -0
  4. package/dist/components/AlignMenu.js +61 -0
  5. package/dist/components/FloatingToolbar.d.ts +4 -0
  6. package/dist/components/FloatingToolbar.js +97 -0
  7. package/dist/components/FormatButtons.d.ts +2 -0
  8. package/dist/components/FormatButtons.js +22 -0
  9. package/dist/components/ImageNavButton.d.ts +7 -0
  10. package/dist/components/ImageNavButton.js +14 -0
  11. package/dist/components/ImageZoomModal.d.ts +12 -0
  12. package/dist/components/ImageZoomModal.js +129 -0
  13. package/dist/components/LayerPanel.d.ts +4 -0
  14. package/dist/components/LayerPanel.js +223 -0
  15. package/dist/components/LoadingOverlay.d.ts +9 -0
  16. package/dist/components/LoadingOverlay.js +17 -0
  17. package/dist/components/ProductEditor.d.ts +4 -0
  18. package/dist/components/ProductEditor.js +67 -0
  19. package/dist/components/ProductImageGallery.d.ts +6 -0
  20. package/dist/components/ProductImageGallery.js +148 -0
  21. package/dist/components/ProductPreviews.d.ts +7 -0
  22. package/dist/components/ProductPreviews.js +281 -0
  23. package/dist/components/TextToolbar.d.ts +4 -0
  24. package/dist/components/TextToolbar.js +117 -0
  25. package/dist/components/Toolbar.d.ts +3 -0
  26. package/dist/components/Toolbar.js +47 -0
  27. package/dist/config/colorConfig.d.ts +1 -0
  28. package/dist/config/colorConfig.js +10 -0
  29. package/dist/config/fontConfig.d.ts +5 -0
  30. package/dist/config/fontConfig.js +21 -0
  31. package/dist/context/ProductEditorContext.d.ts +60 -0
  32. package/dist/context/ProductEditorContext.js +743 -0
  33. package/dist/hooks/useSwipeNavigate.d.ts +11 -0
  34. package/dist/hooks/useSwipeNavigate.js +35 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +30 -0
  37. package/dist/styles/BottomPreviewDisplay.css +65 -0
  38. package/dist/styles/FloatingToolbar.css +41 -0
  39. package/dist/styles/ImageGallery.css +147 -0
  40. package/dist/styles/ImageNavButton.css +35 -0
  41. package/dist/styles/ImageZoomModal.css +101 -0
  42. package/dist/styles/LayerPanel.css +179 -0
  43. package/dist/styles/LoadingOverlay.css +48 -0
  44. package/dist/styles/ProductEditor.css +192 -0
  45. package/dist/styles/TextToolbar.css +327 -0
  46. package/dist/styles/Toolbar.css +125 -0
  47. package/dist/styles/index.css +5 -0
  48. package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
  49. package/dist/styles/textToolbarSelectStyles.js +30 -0
  50. package/dist/types.d.ts +228 -0
  51. package/dist/types.js +17 -0
  52. package/dist/utils/canvasAddText.d.ts +11 -0
  53. package/dist/utils/canvasAddText.js +66 -0
  54. package/dist/utils/canvasUtils.d.ts +37 -0
  55. package/dist/utils/canvasUtils.js +342 -0
  56. package/dist/utils/deviceUtils.d.ts +5 -0
  57. package/dist/utils/deviceUtils.js +33 -0
  58. package/dist/utils/draftTemplateUtils.d.ts +23 -0
  59. package/dist/utils/draftTemplateUtils.js +66 -0
  60. package/dist/utils/grid.d.ts +32 -0
  61. package/dist/utils/grid.js +151 -0
  62. package/dist/utils/imageUtils.d.ts +12 -0
  63. package/dist/utils/imageUtils.js +178 -0
  64. package/dist/utils/job.d.ts +40 -0
  65. package/dist/utils/job.js +115 -0
  66. package/dist/utils/keyboard.d.ts +1 -0
  67. package/dist/utils/keyboard.js +26 -0
  68. package/dist/utils/previewUtils.d.ts +9 -0
  69. package/dist/utils/previewUtils.js +39 -0
  70. package/dist/utils/psdConverter.d.ts +40 -0
  71. package/dist/utils/psdConverter.js +516 -0
  72. package/dist/utils/psdRenderUtils.d.ts +12 -0
  73. package/dist/utils/psdRenderUtils.js +465 -0
  74. package/dist/utils/renderUtils.d.ts +2 -0
  75. package/dist/utils/renderUtils.js +85 -0
  76. package/package.json +66 -0
  77. package/src/components/AlignMenu.tsx +49 -0
  78. package/src/components/FloatingToolbar.tsx +83 -0
  79. package/src/components/FormatButtons.tsx +31 -0
  80. package/src/components/ImageNavButton.tsx +23 -0
  81. package/src/components/ImageZoomModal.tsx +163 -0
  82. package/src/components/LayerPanel.tsx +256 -0
  83. package/src/components/LoadingOverlay.tsx +27 -0
  84. package/src/components/ProductEditor.tsx +104 -0
  85. package/src/components/ProductImageGallery.tsx +181 -0
  86. package/src/components/ProductPreviews.tsx +243 -0
  87. package/src/components/TextToolbar.tsx +164 -0
  88. package/src/components/Toolbar.tsx +86 -0
  89. package/src/config/colorConfig.ts +7 -0
  90. package/src/config/fontConfig.ts +23 -0
  91. package/src/context/ProductEditorContext.tsx +829 -0
  92. package/src/hooks/useSwipeNavigate.ts +40 -0
  93. package/src/index.ts +6 -0
  94. package/src/styles/BottomPreviewDisplay.css +65 -0
  95. package/src/styles/FloatingToolbar.css +41 -0
  96. package/src/styles/ImageGallery.css +147 -0
  97. package/src/styles/ImageNavButton.css +35 -0
  98. package/src/styles/ImageZoomModal.css +101 -0
  99. package/src/styles/LayerPanel.css +179 -0
  100. package/src/styles/LoadingOverlay.css +48 -0
  101. package/src/styles/ProductEditor.css +192 -0
  102. package/src/styles/TextToolbar.css +327 -0
  103. package/src/styles/Toolbar.css +125 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/textToolbarSelectStyles.ts +60 -0
  106. package/src/types/ag-psd/index.d.ts +1 -0
  107. package/src/types/fabric.d.ts +14 -0
  108. package/src/types/lodash/index.d.ts +1 -0
  109. package/src/types/psd.d.ts +53 -0
  110. package/src/types.ts +247 -0
  111. package/src/utils/canvasAddText.ts +72 -0
  112. package/src/utils/canvasUtils.ts +406 -0
  113. package/src/utils/deviceUtils.ts +31 -0
  114. package/src/utils/draftTemplateUtils.ts +70 -0
  115. package/src/utils/grid.ts +154 -0
  116. package/src/utils/imageUtils.ts +218 -0
  117. package/src/utils/job.ts +128 -0
  118. package/src/utils/keyboard.ts +28 -0
  119. package/src/utils/previewUtils.ts +43 -0
  120. package/src/utils/psdConverter.ts +595 -0
  121. package/src/utils/psdRenderUtils.ts +453 -0
  122. package/src/utils/renderUtils.ts +44 -0
  123. package/tsconfig.json +17 -0
@@ -0,0 +1,181 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import ImageNavButton from "./ImageNavButton";
4
+ import ImageZoomModal from "./ImageZoomModal";
5
+ import { ZoomIn } from 'grommet-icons';
6
+ import { useProductEditor } from '../context/ProductEditorContext';
7
+ import { useSwipeNavigate } from '../hooks/useSwipeNavigate';
8
+
9
+ interface ImageGalleryProps {
10
+ fallbackImageUrl?: string;
11
+ }
12
+
13
+ const ProductImageGallery: React.FC<ImageGalleryProps> = ({ fallbackImageUrl = '' }) => {
14
+ const { product } = useProductEditor();
15
+ const { featureImage = null, images = [], name: productName = 'Loading...' } = product;
16
+ const _images = featureImage ? [featureImage, ...images] : images;
17
+ const allImages = _images.map(image => {
18
+ if (!image) return { viewUrl: '/blueman_white.png' };
19
+ return { viewUrl: image.viewUrl };
20
+ });
21
+ const totalImages = allImages.length;
22
+ // image browser related states
23
+ const [slideDirection, setSlideDirection] = useState<'left' | 'right'>('right');
24
+ const [isZoomModalOpen, setIsZoomModalOpen] = useState(false);
25
+ const [isImageLoading, setIsImageLoading] = useState(true);
26
+
27
+ const [currentIndex, setCurrentIndex] = useState(0);
28
+ const [currentImageUrl, setCurrentImageUrl] = useState(
29
+ allImages[currentIndex]?.viewUrl || fallbackImageUrl || '/blueman_white.png'
30
+ );
31
+
32
+ useEffect(() => {
33
+ // set current image URL (for normal image mode)
34
+ if (allImages && allImages.length > 0) {
35
+ setCurrentImageUrl(allImages[currentIndex]?.viewUrl || '/blueman_white.png');
36
+ }
37
+ }, [allImages, currentIndex, fallbackImageUrl]);
38
+
39
+ const handleNavigation = (newDirection: 'left' | 'right') => {
40
+ if (!allImages || allImages.length <= 1) return;
41
+
42
+ setSlideDirection(newDirection === 'left' ? 'left' : 'right');
43
+
44
+ let newIndex;
45
+ const actualTotalImages = Math.min(totalImages, allImages.length);
46
+
47
+ if (newDirection === 'left') {
48
+ newIndex = currentIndex === 0 ? actualTotalImages - 1 : currentIndex - 1;
49
+ } else {
50
+ newIndex = currentIndex === actualTotalImages - 1 ? 0 : currentIndex + 1;
51
+ }
52
+
53
+ setCurrentIndex(newIndex);
54
+ setCurrentImageUrl(allImages[newIndex]?.viewUrl || '/blueman_white.png');
55
+ };
56
+
57
+ const handleThumbnailClick = (index: number) => {
58
+ setCurrentIndex(index);
59
+ setCurrentImageUrl(allImages[index]?.viewUrl || '/blueman_white.png');
60
+ };
61
+
62
+ // Use the swipe hook
63
+ const swipeHandlers = useSwipeNavigate({
64
+ onSwipeLeft: () => handleNavigation('right'),
65
+ onSwipeRight: () => handleNavigation('left'),
66
+ });
67
+
68
+ const slideVariants = {
69
+ enter: (direction: 'left' | 'right') => ({
70
+ x: direction === 'left' ? -1000 : 1000,
71
+ opacity: 0
72
+ }),
73
+ center: {
74
+ zIndex: 1,
75
+ x: 0,
76
+ opacity: 1
77
+ },
78
+ exit: (direction: 'left' | 'right') => ({
79
+ zIndex: 0,
80
+ x: direction === 'left' ? 1000 : -1000,
81
+ opacity: 0
82
+ })
83
+ };
84
+
85
+ return (
86
+ <>
87
+ <div className="image-gallery-container relative">
88
+ <div
89
+ className="image-container overflow-hidden"
90
+ >
91
+ <AnimatePresence initial={false} custom={slideDirection}>
92
+ <motion.div
93
+ key={currentIndex}
94
+ custom={slideDirection}
95
+ variants={slideVariants}
96
+ initial="enter"
97
+ animate="center"
98
+ exit="exit"
99
+ transition={{
100
+ x: { type: "spring", stiffness: 300, damping: 30 },
101
+ opacity: { duration: 0.2 }
102
+ }}
103
+ className="image-slide"
104
+ {...swipeHandlers}
105
+ >
106
+ <img
107
+ src={currentImageUrl}
108
+ alt={productName || "Product Image"}
109
+ className={`gallery-image ${isImageLoading ? '' : 'image-loaded'}`}
110
+ onLoad={() => setIsImageLoading(false)}
111
+ />
112
+ {isImageLoading && (
113
+ <div className="loading-indicator">
114
+ <div className="loading-spinner"></div>
115
+ </div>
116
+ )}
117
+ </motion.div>
118
+ </AnimatePresence>
119
+
120
+ <button
121
+ onClick={() => setIsZoomModalOpen(true)}
122
+ className="zoom-button"
123
+ type="button"
124
+ >
125
+ <ZoomIn className="zoom-icon" />
126
+ </button>
127
+ </div>
128
+ {allImages.length > 1 && (
129
+ <div className="nav-buttons-container">
130
+ <ImageNavButton
131
+ direction="left"
132
+ onClick={() => handleNavigation('left')}
133
+ />
134
+ <ImageNavButton
135
+ direction="right"
136
+ onClick={() => handleNavigation('right')}
137
+ />
138
+ </div>
139
+ )}
140
+ </div>
141
+
142
+ {allImages.length > 1 && (
143
+ <div className="thumbnails-container">
144
+ <div className="thumbnails-row">
145
+ {allImages.slice(0, 5).map((image, index) => {
146
+ const imageSrc = image.viewUrl || "/blueman_white.png";
147
+ const isSelected = index === currentIndex;
148
+
149
+ return (
150
+ <button
151
+ key={index}
152
+ onClick={() => handleThumbnailClick(index)}
153
+ className={`thumbnail-button ${isSelected ? 'selected' : ''}`}
154
+ type="button"
155
+ >
156
+ <img
157
+ src={imageSrc}
158
+ loading="lazy"
159
+ alt={`Product Image ${index + 1}`}
160
+ className="thumbnail-image"
161
+ />
162
+ </button>
163
+ );
164
+ })}
165
+ </div>
166
+ </div>
167
+ )}
168
+ <ImageZoomModal
169
+ isOpen={isZoomModalOpen}
170
+ onClose={() => setIsZoomModalOpen(false)}
171
+ imageUrl={currentImageUrl}
172
+ productName={productName}
173
+ totalImages={allImages.length}
174
+ currentIndex={currentIndex}
175
+ allImages={allImages}
176
+ />
177
+ </>
178
+ );
179
+ };
180
+
181
+ export default ProductImageGallery;
@@ -0,0 +1,243 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import ImageZoomModal from './ImageZoomModal';
3
+ import '../styles/ProductEditor.css';
4
+ import '../styles/BottomPreviewDisplay.css';
5
+ import { useProductEditor } from '../context/ProductEditorContext';
6
+ import { mapPreviewsWithRendered } from '../utils/draftTemplateUtils';
7
+ import { renderDraftPreviewsWithLayers } from '../utils/psdRenderUtils';
8
+
9
+ interface BottomPreviewDisplayProps { }
10
+
11
+ const ProductPreviews: React.FC<BottomPreviewDisplayProps> = () => {
12
+ const {
13
+ draftTemplates,
14
+ draftPreviews,
15
+ loadingPreviews,
16
+ renderedDraftPreviews,
17
+ } = useProductEditor();
18
+ const [isModalOpen, setIsModalOpen] = useState(false);
19
+ const [selectedPreviewIndex, setSelectedPreviewIndex] = useState<number>(0);
20
+ const [renderedPreviews, setRenderedPreviews] = useState<{ draftPreviewId: number | undefined; pngDataUrl: string }[]>([]);
21
+ const [isProcessing, setIsProcessing] = useState(false);
22
+
23
+ // Use refs instead of state to track processed data without causing re-renders
24
+ const processedPreviewIdsRef = useRef<Set<number | undefined>>(new Set());
25
+ // Track which template versions we've already processed
26
+ const processedTemplateVersionsRef = useRef<Map<number, string>>(new Map());
27
+
28
+ // For debugging
29
+ const renderCountRef = useRef(0);
30
+ // Track if this is first mount
31
+ const isFirstMountRef = useRef(true);
32
+
33
+ // When mappedPreviews or renderedDraftPreviews changes, update our rendered PNGs
34
+ useEffect(() => {
35
+ // Skip first render to avoid processing before renderedDraftPreviews are available
36
+ if (isFirstMountRef.current) {
37
+ isFirstMountRef.current = false;
38
+ return;
39
+ }
40
+
41
+ renderCountRef.current++;
42
+
43
+ const processPreviewsWithLayers = async () => {
44
+
45
+ // Skip if we're already processing or if we don't have previews
46
+ if (isProcessing || !draftPreviews || draftPreviews.length === 0) {
47
+ return;
48
+ }
49
+
50
+ // Map previews with their rendered layers
51
+ const mappedPreviews = mapPreviewsWithRendered(draftTemplates, draftPreviews, renderedDraftPreviews);
52
+
53
+ // Skip if there are no mappedPreviews to process
54
+ if (!mappedPreviews || mappedPreviews.length === 0) {
55
+ return;
56
+ }
57
+
58
+ // Check if renderedDraftPreviews have changed compared to what we've processed before
59
+ let changedTemplateIds = new Set<number>();
60
+ renderedDraftPreviews.forEach(rdp => {
61
+ const templateId = rdp.templateId;
62
+ const templateVersion = rdp.draft; // Use the image data URL as a version identifier
63
+ const previousVersion = processedTemplateVersionsRef.current.get(templateId);
64
+
65
+ if (previousVersion !== templateVersion) {
66
+ changedTemplateIds.add(templateId);
67
+ // Update the stored version
68
+ processedTemplateVersionsRef.current.set(templateId, templateVersion);
69
+ }
70
+ });
71
+
72
+ // Only process previews that haven't been processed yet or have changed
73
+ const previewsToProcess = mappedPreviews.filter(mp => {
74
+ const previewId = mp.draftPreview.id;
75
+
76
+ // Always process if we haven't processed this preview yet
77
+ if (!processedPreviewIdsRef.current.has(previewId)) {
78
+ return true;
79
+ }
80
+
81
+ // If no templates have changed, skip reprocessing
82
+ if (changedTemplateIds.size === 0) {
83
+ return false;
84
+ }
85
+
86
+ // Check if this preview uses any templates that have changed
87
+ const shouldProcess = mp.draftPreviewLayers.some(layer =>
88
+ layer.renderedLayer && changedTemplateIds.has(layer.renderedLayer.templateId));
89
+
90
+ return shouldProcess;
91
+ });
92
+
93
+ // Skip if there's nothing new to process
94
+ if (previewsToProcess.length === 0) {
95
+ return;
96
+ }
97
+
98
+ setIsProcessing(true);
99
+
100
+ try {
101
+ // Process previews with our new function
102
+ const processedResults = await renderDraftPreviewsWithLayers(previewsToProcess);
103
+
104
+ // Update our rendered previews state
105
+ setRenderedPreviews(prevRendered => {
106
+ const newRendered = [...prevRendered];
107
+
108
+ processedResults.forEach(result => {
109
+ const existingIndex = newRendered.findIndex(
110
+ r => r.draftPreviewId === result.draftPreviewId
111
+ );
112
+
113
+ if (existingIndex !== -1) {
114
+ // Update existing preview
115
+ newRendered[existingIndex] = result;
116
+ } else {
117
+ // Add new preview
118
+ newRendered.push(result);
119
+ }
120
+ });
121
+
122
+ return newRendered;
123
+ });
124
+
125
+ // Update our processed IDs ref (not state)
126
+ processedResults.forEach(result => {
127
+ if (result.draftPreviewId !== undefined) {
128
+ processedPreviewIdsRef.current.add(result.draftPreviewId);
129
+ }
130
+ });
131
+ } catch (error) {
132
+ console.error('Failed to process previews with layers:', error);
133
+ } finally {
134
+ setIsProcessing(false);
135
+ }
136
+ };
137
+
138
+ processPreviewsWithLayers();
139
+ }, [draftTemplates, draftPreviews, renderedDraftPreviews, isProcessing]);
140
+
141
+ // One-time initialization to handle initial rendering of previews
142
+ // when component mounts and renderedDraftPreviews are available
143
+ useEffect(() => {
144
+ if (draftPreviews?.length > 0 && renderedDraftPreviews?.length > 0 && renderedPreviews.length === 0) {
145
+ const mappedPreviews = mapPreviewsWithRendered(draftTemplates, draftPreviews, renderedDraftPreviews);
146
+
147
+ if (mappedPreviews?.length > 0) {
148
+ (async () => {
149
+ setIsProcessing(true);
150
+ try {
151
+ const processedResults = await renderDraftPreviewsWithLayers(mappedPreviews);
152
+
153
+ // Store initial results
154
+ setRenderedPreviews(processedResults);
155
+
156
+ // Mark these previews as processed
157
+ processedResults.forEach(result => {
158
+ if (result.draftPreviewId !== undefined) {
159
+ processedPreviewIdsRef.current.add(result.draftPreviewId);
160
+ }
161
+ });
162
+
163
+ // Store initial template versions
164
+ renderedDraftPreviews.forEach(rdp => {
165
+ processedTemplateVersionsRef.current.set(rdp.templateId, rdp.draft);
166
+ });
167
+
168
+ } catch (error) {
169
+ console.error('[Initialization] Error processing previews:', error);
170
+ } finally {
171
+ setIsProcessing(false);
172
+ }
173
+ })();
174
+ }
175
+ }
176
+ }, [draftPreviews, renderedDraftPreviews, draftTemplates, renderedPreviews.length]);
177
+
178
+ const openModal = (index: number) => {
179
+ setSelectedPreviewIndex(index);
180
+ setIsModalOpen(true);
181
+ };
182
+
183
+ const closeModal = () => {
184
+ setIsModalOpen(false);
185
+ };
186
+
187
+ // Determine the URL to display for each preview
188
+ const previewUrlsForDisplay = draftPreviews.length > 0
189
+ ? draftPreviews.map(preview => {
190
+ // If we have a rendered PNG for this preview, use it
191
+ const renderedPreview = renderedPreviews.find(rp => rp.draftPreviewId === preview.id);
192
+ return renderedPreview?.pngDataUrl || '';
193
+ })
194
+ : renderedDraftPreviews.map(preview => {
195
+ // If we have a rendered PNG for this preview, use it
196
+ return preview.canvasPreview;
197
+ });
198
+
199
+ const previews = draftPreviews?.length > 0 ? draftPreviews : renderedDraftPreviews;
200
+ return (
201
+ <>
202
+ {!isModalOpen && (
203
+ <div className="bottom-preview-section">
204
+ <div className="preview-images">
205
+ {previews.map((preview: any, index) => loadingPreviews || isProcessing ? (
206
+ <div key={`loading-${preview.id || index}`} className='preview-image-box'>
207
+ <div className="preview-image-box-loading-spinner" />
208
+ </div>
209
+ ) : (
210
+ <div
211
+ key={preview.id || `preview-${index}`}
212
+ className="preview-image-box"
213
+ onClick={() => openModal(index)}
214
+ style={{
215
+ backgroundImage: `url(${previewUrlsForDisplay[index] || preview.file?.viewUrl || ''})`,
216
+ backgroundSize: 'contain',
217
+ backgroundRepeat: 'no-repeat',
218
+ backgroundPosition: 'center center'
219
+ }}
220
+ title={`Preview ${index + 1}`}
221
+ />
222
+ ))}
223
+ </div>
224
+ </div>
225
+ )}
226
+
227
+ <ImageZoomModal
228
+ isOpen={isModalOpen}
229
+ onClose={closeModal}
230
+ imageUrl={previewUrlsForDisplay[selectedPreviewIndex] || (previews[selectedPreviewIndex] as any)?.file?.viewUrl || ''}
231
+ productName="Preview"
232
+ totalImages={previews.length}
233
+ currentIndex={selectedPreviewIndex}
234
+ allImages={previews.map((preview: any, index) => ({
235
+ ...preview,
236
+ key: preview.id || `modal-preview-${index}`
237
+ }))}
238
+ />
239
+ </>
240
+ );
241
+ };
242
+
243
+ export default ProductPreviews;
@@ -0,0 +1,164 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Select from 'react-select';
3
+ import { ChromePicker, ColorResult } from 'react-color';
4
+ import { useProductEditor } from '../context/ProductEditorContext';
5
+ import { FontOption } from '../config/fontConfig';
6
+ import { customStyles } from '../styles/textToolbarSelectStyles';
7
+ import '../styles/TextToolbar.css';
8
+ import { FormatButtons } from './FormatButtons';
9
+ import { AlignMenu } from './AlignMenu';
10
+
11
+ const TextToolbar: React.FC = () => {
12
+ const {
13
+ selectedTextObject,
14
+ updateSelectedText,
15
+ fontOptions,
16
+ colorPalette,
17
+ } = useProductEditor();
18
+ const [displayColorPicker, setDisplayColorPicker] = useState(false);
19
+ const [pickerMode, setPickerMode] = useState<'palette' | 'custom'>('palette');
20
+ const colorPickerRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ const handleClickOutside = (event: MouseEvent) => {
24
+ if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
25
+ setDisplayColorPicker(false);
26
+ }
27
+ };
28
+ document.addEventListener('mousedown', handleClickOutside);
29
+ return () => {
30
+ document.removeEventListener('mousedown', handleClickOutside);
31
+ };
32
+ }, []);
33
+
34
+ if (!selectedTextObject || !updateSelectedText) {
35
+ return null;
36
+ }
37
+
38
+ const currentFontValue = selectedTextObject.fontFamily || 'Nunito';
39
+ const currentColor = typeof selectedTextObject.fill === 'string' ? selectedTextObject.fill : '#000000';
40
+
41
+ const handleFontChange = (selectedOption: FontOption | null) => {
42
+ if (selectedOption) {
43
+ updateSelectedText({ fontFamily: selectedOption.value });
44
+ }
45
+ };
46
+
47
+ const handlePaletteColorClick = (color: string) => {
48
+ updateSelectedText({ fill: color });
49
+ setDisplayColorPicker(false);
50
+ setPickerMode('palette');
51
+ };
52
+
53
+ const handleCustomColorChange = (color: ColorResult) => {
54
+ updateSelectedText({ fill: color.hex });
55
+ };
56
+
57
+ const handleSwatchClick = () => {
58
+ const currentlyVisible = displayColorPicker;
59
+ setDisplayColorPicker(!currentlyVisible);
60
+ if (!currentlyVisible) {
61
+ setPickerMode('palette');
62
+ }
63
+ };
64
+
65
+ const handleCustomTriggerClick = () => {
66
+ setPickerMode('custom');
67
+ setDisplayColorPicker(true);
68
+ };
69
+
70
+ const currentFontOption = fontOptions.find(option => option.value === currentFontValue) || fontOptions[0];
71
+
72
+ return (
73
+ <div className="text-toolbar">
74
+ <Select<FontOption>
75
+ options={fontOptions}
76
+ styles={customStyles}
77
+ value={currentFontOption}
78
+ onChange={handleFontChange}
79
+ isSearchable={true}
80
+ placeholder="Select or search font..."
81
+ className="react-select-container"
82
+ classNamePrefix="react-select"
83
+ />
84
+
85
+ <div className="color-picker-trigger" onClick={handleSwatchClick} title="Change text color">
86
+ <div
87
+ className="color-picker-swatch"
88
+ style={{
89
+ backgroundColor: currentColor,
90
+ borderRadius: '50%',
91
+ width: '28px',
92
+ height: '28px',
93
+ minWidth: '28px',
94
+ minHeight: '28px',
95
+ flexShrink: 0
96
+ }}
97
+ />
98
+ <span className="color-picker-label">Color</span>
99
+ </div>
100
+
101
+ {displayColorPicker && (
102
+ <div className="color-picker-container" ref={colorPickerRef}>
103
+ <div className="color-picker-popover">
104
+ {pickerMode === 'palette' ? (
105
+ <div className="palette-grid">
106
+ {colorPalette.flat().map((color, index) => {
107
+ if (color === null) {
108
+ return (
109
+ <button
110
+ key="custom-trigger"
111
+ className="palette-trigger-swatch"
112
+ onClick={handleCustomTriggerClick}
113
+ title="Choose custom color"
114
+ >
115
+ +
116
+ </button>
117
+ );
118
+ } else {
119
+ return (
120
+ <button
121
+ key={`${color}-${index}`}
122
+ className="palette-swatch"
123
+ style={{ backgroundColor: color }}
124
+ onClick={() => handlePaletteColorClick(color)}
125
+ title={color}
126
+ />
127
+ );
128
+ }
129
+ })}
130
+ </div>
131
+ ) : (
132
+ <>
133
+ <ChromePicker
134
+ color={currentColor}
135
+ onChange={handleCustomColorChange}
136
+ disableAlpha={true}
137
+ styles={{
138
+ default: {
139
+ picker: {
140
+ boxShadow: 'none',
141
+ width: '250px'
142
+ }
143
+ }
144
+ }}
145
+ />
146
+ <button
147
+ className="color-picker-back-btn"
148
+ onClick={() => setPickerMode('palette')}
149
+ >
150
+ Back to Palette
151
+ </button>
152
+ </>
153
+ )}
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ <FormatButtons />
159
+ <AlignMenu />
160
+ </div>
161
+ );
162
+ };
163
+
164
+ export default TextToolbar;
@@ -0,0 +1,86 @@
1
+ import React from 'react';
2
+ import { Apps, View, Trash, Layer } from 'grommet-icons';
3
+ import { useProductEditor } from '../context/ProductEditorContext';
4
+ import { addTextToCanvas } from '../utils/canvasUtils';
5
+ import '../styles/Toolbar.css';
6
+
7
+ export default function Toolbar() {
8
+ const {
9
+ canvas,
10
+ width,
11
+ setShowGrid,
12
+ showGrid,
13
+ height,
14
+ showPreview,
15
+ togglePreview,
16
+ showLayerPanel,
17
+ toggleLayerPanel,
18
+ deleteObject,
19
+ selectedObjectId,
20
+ renderedDraftPreviews
21
+ } = useProductEditor();
22
+
23
+ // Add handleAddText function before the return statement
24
+ const handleAddText = () => {
25
+ if (!canvas) return;
26
+ addTextToCanvas(canvas, width, height);
27
+ };
28
+
29
+ const toggleGrid = () => setShowGrid(!showGrid);
30
+
31
+ // Handle delete button click
32
+ const handleDelete = () => {
33
+ if (!canvas) return;
34
+ const activeObject = canvas.getActiveObject();
35
+ if (activeObject) {
36
+ deleteObject(activeObject);
37
+ }
38
+ };
39
+
40
+ // Check if previews are available
41
+ const hasPreview = renderedDraftPreviews && renderedDraftPreviews.length > 0;
42
+
43
+ return (
44
+ <>
45
+ {/* Grid toggle button */}
46
+ <div className="grid-toggle">
47
+ <div
48
+ className={`toolbar-button ${showGrid ? 'active' : ''}`}
49
+ onClick={toggleGrid}
50
+ >
51
+ <Apps width={24} height={24} />
52
+ <span>{showGrid ? 'Hide Grid' : 'Show Grid'}</span>
53
+ </div>
54
+ </div>
55
+
56
+ {/* Preview toggle button */}
57
+ <div className="preview-toggle">
58
+ <div
59
+ className={`toolbar-button ${showPreview && hasPreview ? 'active' : ''} ${!hasPreview ? 'disabled' : ''}`}
60
+ onClick={hasPreview ? togglePreview : undefined}
61
+ >
62
+ <View width={24} height={24} />
63
+ <span>{hasPreview && showPreview ? 'Hide Preview' : 'Show Preview'}</span>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Layer button */}
68
+ <div
69
+ className={`toolbar-button ${showLayerPanel ? 'active' : ''}`}
70
+ onClick={toggleLayerPanel}
71
+ >
72
+ <Layer width={24} height={24} />
73
+ <span>Layer</span>
74
+ </div>
75
+
76
+ {/* Delete button */}
77
+ <div
78
+ className={`toolbar-button ${!selectedObjectId ? 'disabled' : ''}`}
79
+ onClick={handleDelete}
80
+ >
81
+ <Trash width={24} height={24} />
82
+ <span>Delete</span>
83
+ </div>
84
+ </>
85
+ );
86
+ }
@@ -0,0 +1,7 @@
1
+ export const defaultPalette: (string | null)[][] = [
2
+ [null, '#000000', '#434343', '#666666', '#999999', '#CCCCCC', '#D9D9D9', '#EFEFEF'],
3
+ ['#FFFFFF', '#F44E3B', '#FE9200', '#FCDC00', '#DBDF00', '#A4DD00', '#68CCCA', '#73D8FF'],
4
+ ['#AEA1FF', '#FDA1FF', '#CF329A', '#FF6900', '#FCB900', '#7BDCB5', '#00D084', '#8ED1FC'],
5
+ ['#0693E3', '#EB144C', '#F78DA7', '#9900EF', '#B80000', '#DB3E00', '#FCC400', '#27724D'],
6
+ ['#607D8B', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548']
7
+ ];