@jhits/plugin-images 0.0.7 → 0.0.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 (102) hide show
  1. package/package.json +8 -9
  2. package/src/api/fallback/route.ts +0 -69
  3. package/src/api/index.ts +0 -10
  4. package/src/api/list/index.ts +0 -96
  5. package/src/api/resolve/route.ts +0 -241
  6. package/src/api/router.ts +0 -85
  7. package/src/api/upload/index.ts +0 -88
  8. package/src/api/uploads/[filename]/route.ts +0 -93
  9. package/src/api-server.ts +0 -11
  10. package/src/assets/noimagefound.jpg +0 -0
  11. package/src/components/BackgroundImage.d.ts +0 -11
  12. package/src/components/BackgroundImage.d.ts.map +0 -1
  13. package/src/components/BackgroundImage.js +0 -35
  14. package/src/components/BackgroundImage.tsx +0 -92
  15. package/src/components/GlobalImageEditor/config.d.ts +0 -9
  16. package/src/components/GlobalImageEditor/config.d.ts.map +0 -1
  17. package/src/components/GlobalImageEditor/config.js +0 -18
  18. package/src/components/GlobalImageEditor/config.ts +0 -21
  19. package/src/components/GlobalImageEditor/eventHandlers.d.ts +0 -20
  20. package/src/components/GlobalImageEditor/eventHandlers.d.ts.map +0 -1
  21. package/src/components/GlobalImageEditor/eventHandlers.js +0 -206
  22. package/src/components/GlobalImageEditor/eventHandlers.ts +0 -267
  23. package/src/components/GlobalImageEditor/imageDetection.d.ts +0 -16
  24. package/src/components/GlobalImageEditor/imageDetection.d.ts.map +0 -1
  25. package/src/components/GlobalImageEditor/imageDetection.js +0 -130
  26. package/src/components/GlobalImageEditor/imageDetection.ts +0 -160
  27. package/src/components/GlobalImageEditor/imageSetup.d.ts +0 -9
  28. package/src/components/GlobalImageEditor/imageSetup.d.ts.map +0 -1
  29. package/src/components/GlobalImageEditor/imageSetup.js +0 -261
  30. package/src/components/GlobalImageEditor/imageSetup.ts +0 -306
  31. package/src/components/GlobalImageEditor/saveLogic.d.ts +0 -26
  32. package/src/components/GlobalImageEditor/saveLogic.d.ts.map +0 -1
  33. package/src/components/GlobalImageEditor/saveLogic.js +0 -99
  34. package/src/components/GlobalImageEditor/saveLogic.ts +0 -133
  35. package/src/components/GlobalImageEditor/stylingDetection.d.ts +0 -9
  36. package/src/components/GlobalImageEditor/stylingDetection.d.ts.map +0 -1
  37. package/src/components/GlobalImageEditor/stylingDetection.js +0 -110
  38. package/src/components/GlobalImageEditor/stylingDetection.ts +0 -122
  39. package/src/components/GlobalImageEditor/transformParsing.d.ts +0 -16
  40. package/src/components/GlobalImageEditor/transformParsing.d.ts.map +0 -1
  41. package/src/components/GlobalImageEditor/transformParsing.js +0 -68
  42. package/src/components/GlobalImageEditor/transformParsing.ts +0 -83
  43. package/src/components/GlobalImageEditor/types.d.ts +0 -36
  44. package/src/components/GlobalImageEditor/types.d.ts.map +0 -1
  45. package/src/components/GlobalImageEditor/types.js +0 -4
  46. package/src/components/GlobalImageEditor/types.ts +0 -39
  47. package/src/components/GlobalImageEditor.d.ts +0 -8
  48. package/src/components/GlobalImageEditor.d.ts.map +0 -1
  49. package/src/components/GlobalImageEditor.js +0 -227
  50. package/src/components/GlobalImageEditor.tsx +0 -327
  51. package/src/components/Image.d.ts +0 -22
  52. package/src/components/Image.d.ts.map +0 -1
  53. package/src/components/Image.js +0 -229
  54. package/src/components/Image.tsx +0 -343
  55. package/src/components/ImageBrowserModal.d.ts +0 -13
  56. package/src/components/ImageBrowserModal.d.ts.map +0 -1
  57. package/src/components/ImageBrowserModal.js +0 -504
  58. package/src/components/ImageBrowserModal.tsx +0 -837
  59. package/src/components/ImageEditor.d.ts +0 -27
  60. package/src/components/ImageEditor.d.ts.map +0 -1
  61. package/src/components/ImageEditor.js +0 -173
  62. package/src/components/ImageEditor.tsx +0 -323
  63. package/src/components/ImageEffectsPanel.tsx +0 -116
  64. package/src/components/ImagePicker.d.ts +0 -3
  65. package/src/components/ImagePicker.d.ts.map +0 -1
  66. package/src/components/ImagePicker.js +0 -143
  67. package/src/components/ImagePicker.tsx +0 -265
  68. package/src/components/ImagesPluginInit.d.ts +0 -24
  69. package/src/components/ImagesPluginInit.d.ts.map +0 -1
  70. package/src/components/ImagesPluginInit.js +0 -28
  71. package/src/components/ImagesPluginInit.tsx +0 -31
  72. package/src/components/index.ts +0 -10
  73. package/src/config.ts +0 -179
  74. package/src/hooks/useImagePicker.d.ts +0 -20
  75. package/src/hooks/useImagePicker.d.ts.map +0 -1
  76. package/src/hooks/useImagePicker.js +0 -322
  77. package/src/hooks/useImagePicker.ts +0 -344
  78. package/src/index.d.ts +0 -23
  79. package/src/index.d.ts.map +0 -1
  80. package/src/index.js +0 -28
  81. package/src/index.server.ts +0 -12
  82. package/src/index.tsx +0 -56
  83. package/src/init.d.ts +0 -33
  84. package/src/init.d.ts.map +0 -1
  85. package/src/init.js +0 -43
  86. package/src/init.tsx +0 -58
  87. package/src/types/index.d.ts +0 -80
  88. package/src/types/index.d.ts.map +0 -1
  89. package/src/types/index.js +0 -4
  90. package/src/types/index.ts +0 -84
  91. package/src/utils/fallback.d.ts +0 -27
  92. package/src/utils/fallback.d.ts.map +0 -1
  93. package/src/utils/fallback.js +0 -63
  94. package/src/utils/fallback.ts +0 -73
  95. package/src/utils/transforms.d.ts +0 -26
  96. package/src/utils/transforms.d.ts.map +0 -1
  97. package/src/utils/transforms.js +0 -38
  98. package/src/utils/transforms.ts +0 -54
  99. package/src/views/ImageManager.d.ts +0 -10
  100. package/src/views/ImageManager.d.ts.map +0 -1
  101. package/src/views/ImageManager.js +0 -9
  102. package/src/views/ImageManager.tsx +0 -30
@@ -1,3 +0,0 @@
1
- import type { ImagePickerProps } from '../types';
2
- export declare function ImagePicker({ value, onChange, brightness, blur, scale, positionX, positionY, aspectRatio, borderRadius, onBrightnessChange, onBlurChange, onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor, }: ImagePickerProps): import("react/jsx-runtime").JSX.Element;
3
- //# sourceMappingURL=ImagePicker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ImagePicker.d.ts","sourceRoot":"","sources":["ImagePicker.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAQjD,wBAAgB,WAAW,CAAC,EACxB,KAAK,EAAE,QAAQ,EAAE,UAAgB,EAAE,IAAQ,EAAE,KAAW,EAAE,SAAa,EAAE,SAAa,EACtF,WAAoB,EAAE,YAA2B,EAAE,kBAAkB,EAAE,YAAY,EACnF,aAAa,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAsB,GAC5F,EAAE,gBAAgB,2CAuPlB"}
@@ -1,143 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { Search, Image as ImageIcon, Settings } from 'lucide-react';
6
- import { ImageEditor } from './ImageEditor';
7
- import { ImageBrowserModal } from './ImageBrowserModal';
8
- import { useImagePicker } from '../hooks/useImagePicker';
9
- import { getImageTransform, getImageFilter } from '../utils/transforms';
10
- import { getFallbackImageUrl } from '../utils/fallback';
11
- export function ImagePicker({ value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0, aspectRatio = '16/9', borderRadius = 'rounded-xl', onBrightnessChange, onBlurChange, onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor = false, }) {
12
- const [transforms, setTransforms] = useState({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
13
- const [isEditorOpen, setIsEditorOpen] = useState(false);
14
- const [isBrowserOpen, setIsBrowserOpen] = useState(false);
15
- const [previewBaseScale, setPreviewBaseScale] = useState(null);
16
- const [previewImageError, setPreviewImageError] = useState(false);
17
- const [mounted, setMounted] = useState(false);
18
- const [portalTarget, setPortalTarget] = useState(null);
19
- const previewImageRef = useRef(null);
20
- const previewContainerRef = useRef(null);
21
- const editorRef = useRef(null);
22
- const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
23
- // Reset preview error when selected image changes
24
- useEffect(() => {
25
- setPreviewImageError(false);
26
- }, [selectedImage === null || selectedImage === void 0 ? void 0 : selectedImage.id, selectedImage === null || selectedImage === void 0 ? void 0 : selectedImage.url]);
27
- // Handle SSR & portal target - ensure we only render portal on client
28
- useEffect(() => {
29
- var _a;
30
- setMounted(true);
31
- if (typeof document === 'undefined')
32
- return;
33
- // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
34
- // This ensures the browser/editor modals share the same stacking context
35
- const editorContainer = (_a = previewContainerRef.current) === null || _a === void 0 ? void 0 : _a.closest('[data-image-editor="true"]');
36
- if (editorContainer) {
37
- setPortalTarget(editorContainer);
38
- }
39
- else {
40
- setPortalTarget(document.body);
41
- }
42
- }, []);
43
- // Auto-open editor if requested (e.g., when opening from edit button)
44
- // Only auto-open once when the prop first becomes true, not on every render
45
- const hasAutoOpenedRef = useRef(false);
46
- useEffect(() => {
47
- if (autoOpenEditor && selectedImage && !isEditorOpen && !hasAutoOpenedRef.current) {
48
- // Small delay to ensure ImagePicker is fully rendered
49
- const timer = setTimeout(() => {
50
- setIsEditorOpen(true);
51
- hasAutoOpenedRef.current = true;
52
- }, 100);
53
- return () => clearTimeout(timer);
54
- }
55
- // Reset the flag when autoOpenEditor becomes false
56
- if (!autoOpenEditor) {
57
- hasAutoOpenedRef.current = false;
58
- }
59
- }, [autoOpenEditor, selectedImage, isEditorOpen]);
60
- useEffect(() => {
61
- if (!isEditorOpen)
62
- setTransforms({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
63
- }, [scale, positionX, positionY, isEditorOpen]);
64
- const calculatePreviewBaseScale = useCallback(() => {
65
- if (!previewImageRef.current || !previewContainerRef.current)
66
- return;
67
- const fit = Math.max(previewContainerRef.current.offsetWidth / previewImageRef.current.naturalWidth, previewContainerRef.current.offsetHeight / previewImageRef.current.naturalHeight);
68
- setPreviewBaseScale(fit);
69
- }, []);
70
- // Handle editor save - delegate to parent callbacks instead of saving directly
71
- // This ensures all saves go through a single location (GlobalImageEditor.saveImageTransform)
72
- const handleEditorSave = async () => {
73
- if (!editorRef.current || !selectedImage)
74
- return;
75
- try {
76
- // 1. Get current values from Editor UI (this will also call onBrightnessChange and onBlurChange)
77
- const final = await editorRef.current.flushSave();
78
- // 2. Normalize position values - if they're -50% (centering value), treat as 0
79
- const normalizedPositionX = final.positionX === -50 ? 0 : final.positionX;
80
- const normalizedPositionY = final.positionY === -50 ? 0 : final.positionY;
81
- // 3. If onEditorSave is provided, use it exclusively to prevent duplicate saves
82
- // Otherwise, update parent state through individual callbacks
83
- console.log('[ImagePicker] handleEditorSave - final values:', final, 'has onEditorSave:', !!onEditorSave);
84
- if (onEditorSave) {
85
- // onEditorSave handles everything - don't call individual handlers to avoid duplicates
86
- console.log('[ImagePicker] Calling onEditorSave with:', { scale: final.scale, positionX: normalizedPositionX, positionY: normalizedPositionY, brightness: final.brightness, blur: final.blur });
87
- onEditorSave(final.scale, normalizedPositionX, normalizedPositionY, final.brightness, final.blur);
88
- }
89
- else {
90
- // Fallback: update parent state through individual callbacks
91
- // Since onEditorSave is not provided, call all callbacks and they should handle saving
92
- // The last one (onPositionYChange) will trigger the final save
93
- console.log('[ImagePicker] No onEditorSave, using individual callbacks');
94
- // Update scale first
95
- onScaleChange === null || onScaleChange === void 0 ? void 0 : onScaleChange(final.scale);
96
- // Update positions - these will trigger saves
97
- onPositionXChange === null || onPositionXChange === void 0 ? void 0 : onPositionXChange(normalizedPositionX);
98
- // Last callback - this should trigger the final save with all values
99
- // We need to ensure this saves immediately with all final values including brightness/blur
100
- onPositionYChange === null || onPositionYChange === void 0 ? void 0 : onPositionYChange(normalizedPositionY);
101
- }
102
- // 5. Close the editor
103
- setIsEditorOpen(false);
104
- }
105
- catch (error) {
106
- console.error('[ImagePicker] Failed to get editor values:', error);
107
- }
108
- };
109
- const aspectValue = useMemo(() => {
110
- if (aspectRatio === 'auto')
111
- return undefined;
112
- const [w, h] = aspectRatio.split('/').map(Number);
113
- return w / h;
114
- }, [aspectRatio]);
115
- return (_jsxs("div", { className: "space-y-6", children: [selectedImage ? (_jsx("div", { className: "relative group max-w-md mx-auto", children: _jsxs("div", { className: `relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`, style: { aspectRatio: aspectValue, width: '100%' }, children: [_jsx("div", { ref: previewContainerRef, className: "relative w-full h-full overflow-hidden", children: previewImageError ? (_jsx("img", { src: getFallbackImageUrl(), alt: selectedImage.filename || 'Image not found', className: "absolute max-w-none", style: {
116
- top: '50%', left: '50%', width: 'auto', height: 'auto',
117
- minWidth: '100%', minHeight: '100%',
118
- transform: 'translate(-50%, -50%)',
119
- transformOrigin: 'center center',
120
- objectFit: 'cover',
121
- } })) : (_jsx("img", { ref: previewImageRef, src: selectedImage.url, alt: selectedImage.filename, className: "absolute max-w-none", onLoad: calculatePreviewBaseScale, onError: () => setPreviewImageError(true), style: {
122
- top: '50%', left: '50%', width: 'auto', height: 'auto',
123
- minWidth: '100%', minHeight: '100%',
124
- filter: getImageFilter(brightness, blur),
125
- transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
126
- transformOrigin: 'center center',
127
- } })) }), _jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" })] }) })) : (_jsxs("div", { onClick: () => setIsBrowserOpen(true), className: "aspect-video bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-950 rounded-2xl border-2 border-dashed border-neutral-300 dark:border-neutral-800 flex flex-col items-center justify-center text-neutral-400 hover:border-primary hover:text-primary cursor-pointer transition-all duration-300 hover:shadow-lg group", children: [_jsx("div", { className: "p-4 bg-white/50 dark:bg-neutral-800/50 rounded-full mb-3 group-hover:scale-110 transition-transform duration-300", children: _jsx(ImageIcon, { size: 28, className: "group-hover:scale-110 transition-transform duration-300" }) }), _jsx("span", { className: "text-xs font-bold uppercase tracking-[0.15em] group-hover:tracking-[0.2em] transition-all duration-300", children: "Select Image" })] })), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [_jsxs("button", { onClick: () => setIsBrowserOpen(true), className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]", children: [_jsx(Search, { size: 16 }), "Browse"] }), _jsxs("button", { onClick: () => setIsEditorOpen(true), disabled: !selectedImage, className: "flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm disabled:hover:scale-100", children: [_jsx(Settings, { size: 16 }), "Edit"] })] }), isEditorOpen && selectedImage && mounted && portalTarget && createPortal(_jsx("div", { className: "fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200", children: _jsxs("div", { className: "w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300", children: [_jsx("div", { className: "flex-1 overflow-hidden p-4 lg:p-6", children: _jsx(ImageEditor, { ref: editorRef, imageUrl: selectedImage.url, scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, brightness: brightness, blur: blur, onScaleChange: (s) => setTransforms(t => (Object.assign(Object.assign({}, t), { scale: s }))), onPositionChange: (x, y) => {
128
- // Only update local state during drag - don't trigger saves
129
- // Saves will happen when editor closes via onEditorSave
130
- setTransforms(t => (Object.assign(Object.assign({}, t), { positionX: x, positionY: y })));
131
- }, onBrightnessChange: onBrightnessChange, onBlurChange: onBlurChange, aspectRatio: aspectRatio, borderRadius: borderRadius }) }), _jsxs("div", { className: "px-6 py-5 border-t border-neutral-200 dark:border-neutral-800 bg-neutral-50/80 dark:bg-neutral-900/80 backdrop-blur-sm flex justify-end gap-3", children: [_jsx("button", { onClick: () => setIsEditorOpen(false), className: "px-6 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl font-semibold text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]", children: "Cancel" }), _jsx("button", { onClick: handleEditorSave, className: "px-8 py-2.5 bg-primary text-white rounded-xl font-semibold shadow-md hover:shadow-lg hover:bg-primary/90 transition-all duration-200 active:scale-[0.98]", children: "Done" })] })] }) }), portalTarget), _jsx(ImageBrowserModal, { isOpen: isBrowserOpen, onClose: () => setIsBrowserOpen(false), onSelectImage: (image) => {
132
- setSelectedImage(image);
133
- onChange === null || onChange === void 0 ? void 0 : onChange(image);
134
- setIsBrowserOpen(false);
135
- }, selectedImageId: (() => {
136
- // Use resolved image's filename/URL if available, otherwise fall back to value
137
- // This ensures semantic IDs are resolved to actual filenames for matching
138
- if (selectedImage) {
139
- return selectedImage.filename || selectedImage.url || selectedImage.id || value;
140
- }
141
- return value;
142
- })(), darkMode: false })] }));
143
- }
@@ -1,265 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { Search, Image as ImageIcon, Settings } from 'lucide-react';
6
- import type { ImagePickerProps } from '../types';
7
- import type { ImageMetadata } from '../types';
8
- import { ImageEditor, type ImageEditorHandle } from './ImageEditor';
9
- import { ImageBrowserModal } from './ImageBrowserModal';
10
- import { useImagePicker } from '../hooks/useImagePicker';
11
- import { getImageTransform, getImageFilter } from '../utils/transforms';
12
- import { getFallbackImageUrl } from '../utils/fallback';
13
-
14
- export function ImagePicker({
15
- value, onChange, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0,
16
- aspectRatio = '16/9', borderRadius = 'rounded-xl', onBrightnessChange, onBlurChange,
17
- onScaleChange, onPositionXChange, onPositionYChange, onEditorSave, autoOpenEditor = false,
18
- }: ImagePickerProps) {
19
-
20
- const [transforms, setTransforms] = useState({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
21
- const [isEditorOpen, setIsEditorOpen] = useState(false);
22
- const [isBrowserOpen, setIsBrowserOpen] = useState(false);
23
- const [previewBaseScale, setPreviewBaseScale] = useState<number | null>(null);
24
- const [previewImageError, setPreviewImageError] = useState(false);
25
- const [mounted, setMounted] = useState(false);
26
- const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
27
- const previewImageRef = useRef<HTMLImageElement>(null);
28
- const previewContainerRef = useRef<HTMLDivElement>(null);
29
- const editorRef = useRef<ImageEditorHandle | null>(null);
30
-
31
- const { selectedImage, setSelectedImage } = useImagePicker({ value, images: [] });
32
-
33
- // Reset preview error when selected image changes
34
- useEffect(() => {
35
- setPreviewImageError(false);
36
- }, [selectedImage?.id, selectedImage?.url]);
37
-
38
- // Handle SSR & portal target - ensure we only render portal on client
39
- useEffect(() => {
40
- setMounted(true);
41
-
42
- if (typeof document === 'undefined') return;
43
-
44
- // If we're inside the GlobalImageEditor overlay, portal into that instead of document.body
45
- // This ensures the browser/editor modals share the same stacking context
46
- const editorContainer = previewContainerRef.current?.closest('[data-image-editor="true"]') as HTMLElement | null;
47
- if (editorContainer) {
48
- setPortalTarget(editorContainer);
49
- } else {
50
- setPortalTarget(document.body);
51
- }
52
- }, []);
53
-
54
- // Auto-open editor if requested (e.g., when opening from edit button)
55
- // Only auto-open once when the prop first becomes true, not on every render
56
- const hasAutoOpenedRef = useRef(false);
57
- useEffect(() => {
58
- if (autoOpenEditor && selectedImage && !isEditorOpen && !hasAutoOpenedRef.current) {
59
- // Small delay to ensure ImagePicker is fully rendered
60
- const timer = setTimeout(() => {
61
- setIsEditorOpen(true);
62
- hasAutoOpenedRef.current = true;
63
- }, 100);
64
- return () => clearTimeout(timer);
65
- }
66
- // Reset the flag when autoOpenEditor becomes false
67
- if (!autoOpenEditor) {
68
- hasAutoOpenedRef.current = false;
69
- }
70
- }, [autoOpenEditor, selectedImage, isEditorOpen]);
71
-
72
- useEffect(() => {
73
- if (!isEditorOpen) setTransforms({ scale: scale >= 0.1 ? scale : 1.0, positionX, positionY });
74
- }, [scale, positionX, positionY, isEditorOpen]);
75
-
76
- const calculatePreviewBaseScale = useCallback(() => {
77
- if (!previewImageRef.current || !previewContainerRef.current) return;
78
- const fit = Math.max(previewContainerRef.current.offsetWidth / previewImageRef.current.naturalWidth,
79
- previewContainerRef.current.offsetHeight / previewImageRef.current.naturalHeight);
80
- setPreviewBaseScale(fit);
81
- }, []);
82
-
83
- // Handle editor save - delegate to parent callbacks instead of saving directly
84
- // This ensures all saves go through a single location (GlobalImageEditor.saveImageTransform)
85
- const handleEditorSave = async () => {
86
- if (!editorRef.current || !selectedImage) return;
87
-
88
- try {
89
- // 1. Get current values from Editor UI (this will also call onBrightnessChange and onBlurChange)
90
- const final = await editorRef.current.flushSave();
91
-
92
- // 2. Normalize position values - if they're -50% (centering value), treat as 0
93
- const normalizedPositionX = final.positionX === -50 ? 0 : final.positionX;
94
- const normalizedPositionY = final.positionY === -50 ? 0 : final.positionY;
95
-
96
- // 3. If onEditorSave is provided, use it exclusively to prevent duplicate saves
97
- // Otherwise, update parent state through individual callbacks
98
- console.log('[ImagePicker] handleEditorSave - final values:', final, 'has onEditorSave:', !!onEditorSave);
99
- if (onEditorSave) {
100
- // onEditorSave handles everything - don't call individual handlers to avoid duplicates
101
- console.log('[ImagePicker] Calling onEditorSave with:', { scale: final.scale, positionX: normalizedPositionX, positionY: normalizedPositionY, brightness: final.brightness, blur: final.blur });
102
- onEditorSave(final.scale, normalizedPositionX, normalizedPositionY, final.brightness, final.blur);
103
- } else {
104
- // Fallback: update parent state through individual callbacks
105
- // Since onEditorSave is not provided, call all callbacks and they should handle saving
106
- // The last one (onPositionYChange) will trigger the final save
107
- console.log('[ImagePicker] No onEditorSave, using individual callbacks');
108
- // Update scale first
109
- onScaleChange?.(final.scale);
110
- // Update positions - these will trigger saves
111
- onPositionXChange?.(normalizedPositionX);
112
- // Last callback - this should trigger the final save with all values
113
- // We need to ensure this saves immediately with all final values including brightness/blur
114
- onPositionYChange?.(normalizedPositionY);
115
- }
116
-
117
- // 5. Close the editor
118
- setIsEditorOpen(false);
119
- } catch (error) {
120
- console.error('[ImagePicker] Failed to get editor values:', error);
121
- }
122
- };
123
-
124
-
125
- const aspectValue = useMemo(() => {
126
- if (aspectRatio === 'auto') return undefined;
127
- const [w, h] = aspectRatio.split('/').map(Number);
128
- return w / h;
129
- }, [aspectRatio]);
130
-
131
- return (
132
- <div className="space-y-6">
133
- {selectedImage ? (
134
- <div className="relative group max-w-md mx-auto">
135
- <div className={`relative ${borderRadius} overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 shadow-lg transition-all duration-300 hover:shadow-xl`} style={{ aspectRatio: aspectValue, width: '100%' }}>
136
- <div ref={previewContainerRef} className="relative w-full h-full overflow-hidden">
137
- {previewImageError ? (
138
- <img
139
- src={getFallbackImageUrl()}
140
- alt={selectedImage.filename || 'Image not found'}
141
- className="absolute max-w-none"
142
- style={{
143
- top: '50%', left: '50%', width: 'auto', height: 'auto',
144
- minWidth: '100%', minHeight: '100%',
145
- transform: 'translate(-50%, -50%)',
146
- transformOrigin: 'center center',
147
- objectFit: 'cover',
148
- }}
149
- />
150
- ) : (
151
- <img
152
- ref={previewImageRef}
153
- src={selectedImage.url}
154
- alt={selectedImage.filename}
155
- className="absolute max-w-none"
156
- onLoad={calculatePreviewBaseScale}
157
- onError={() => setPreviewImageError(true)}
158
- style={{
159
- top: '50%', left: '50%', width: 'auto', height: 'auto',
160
- minWidth: '100%', minHeight: '100%',
161
- filter: getImageFilter(brightness, blur),
162
- transform: previewBaseScale ? getImageTransform({ scale: transforms.scale, positionX: transforms.positionX, positionY: transforms.positionY, baseScale: previewBaseScale }, true) : 'translate(-50%, -50%)',
163
- transformOrigin: 'center center',
164
- }}
165
- />
166
- )}
167
- </div>
168
- {/* Overlay on hover */}
169
- <div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300 pointer-events-none" />
170
- </div>
171
- </div>
172
- ) : (
173
- <div
174
- onClick={() => setIsBrowserOpen(true)}
175
- className="aspect-video bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-950 rounded-2xl border-2 border-dashed border-neutral-300 dark:border-neutral-800 flex flex-col items-center justify-center text-neutral-400 hover:border-primary hover:text-primary cursor-pointer transition-all duration-300 hover:shadow-lg group"
176
- >
177
- <div className="p-4 bg-white/50 dark:bg-neutral-800/50 rounded-full mb-3 group-hover:scale-110 transition-transform duration-300">
178
- <ImageIcon size={28} className="group-hover:scale-110 transition-transform duration-300" />
179
- </div>
180
- <span className="text-xs font-bold uppercase tracking-[0.15em] group-hover:tracking-[0.2em] transition-all duration-300">Select Image</span>
181
- </div>
182
- )}
183
-
184
- <div className="flex flex-wrap items-center justify-center gap-3">
185
- <button
186
- onClick={() => setIsBrowserOpen(true)}
187
- className="flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]"
188
- >
189
- <Search size={16} />
190
- Browse
191
- </button>
192
- <button
193
- onClick={() => setIsEditorOpen(true)}
194
- disabled={!selectedImage}
195
- className="flex items-center justify-center gap-2 px-5 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm font-semibold text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-750 hover:border-neutral-300 dark:hover:border-neutral-600 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98] disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm disabled:hover:scale-100"
196
- >
197
- <Settings size={16} />
198
- Edit
199
- </button>
200
- </div>
201
-
202
- {isEditorOpen && selectedImage && mounted && portalTarget && createPortal(
203
- <div className="fixed inset-0 z-[200] flex items-center justify-center bg-neutral-950/80 dark:bg-neutral-950/90 backdrop-blur-md p-4 animate-in fade-in duration-200">
204
- <div className="w-full max-w-4xl bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden shadow-2xl dark:shadow-neutral-950/50 flex flex-col max-h-[85vh] border border-neutral-200 dark:border-neutral-800 animate-in zoom-in-95 duration-300">
205
- <div className="flex-1 overflow-hidden p-4 lg:p-6">
206
- <ImageEditor
207
- ref={editorRef}
208
- imageUrl={selectedImage.url}
209
- scale={transforms.scale}
210
- positionX={transforms.positionX}
211
- positionY={transforms.positionY}
212
- brightness={brightness}
213
- blur={blur}
214
- onScaleChange={(s) => setTransforms(t => ({ ...t, scale: s }))}
215
- onPositionChange={(x, y) => {
216
- // Only update local state during drag - don't trigger saves
217
- // Saves will happen when editor closes via onEditorSave
218
- setTransforms(t => ({ ...t, positionX: x, positionY: y }));
219
- }}
220
- onBrightnessChange={onBrightnessChange}
221
- onBlurChange={onBlurChange}
222
- aspectRatio={aspectRatio}
223
- borderRadius={borderRadius}
224
- />
225
- </div>
226
- <div className="px-6 py-5 border-t border-neutral-200 dark:border-neutral-800 bg-neutral-50/80 dark:bg-neutral-900/80 backdrop-blur-sm flex justify-end gap-3">
227
- <button
228
- onClick={() => setIsEditorOpen(false)}
229
- className="px-6 py-2.5 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl font-semibold text-neutral-700 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-200 shadow-sm hover:shadow-md active:scale-[0.98]"
230
- >
231
- Cancel
232
- </button>
233
- <button
234
- onClick={handleEditorSave}
235
- className="px-8 py-2.5 bg-primary text-white rounded-xl font-semibold shadow-md hover:shadow-lg hover:bg-primary/90 transition-all duration-200 active:scale-[0.98]"
236
- >
237
- Done
238
- </button>
239
- </div>
240
- </div>
241
- </div>,
242
- portalTarget
243
- )}
244
-
245
- <ImageBrowserModal
246
- isOpen={isBrowserOpen}
247
- onClose={() => setIsBrowserOpen(false)}
248
- onSelectImage={(image) => {
249
- setSelectedImage(image);
250
- onChange?.(image);
251
- setIsBrowserOpen(false);
252
- }}
253
- selectedImageId={(() => {
254
- // Use resolved image's filename/URL if available, otherwise fall back to value
255
- // This ensures semantic IDs are resolved to actual filenames for matching
256
- if (selectedImage) {
257
- return selectedImage.filename || selectedImage.url || selectedImage.id || value;
258
- }
259
- return value;
260
- })()}
261
- darkMode={false}
262
- />
263
- </div>
264
- );
265
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Images Plugin Initialization Component
3
- *
4
- * This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
5
- * and renders the GlobalImageEditor if enabled.
6
- *
7
- * Render this once in your app layout after calling initImagesPlugin().
8
- */
9
- /**
10
- * Images Plugin Initialization Component
11
- *
12
- * Renders the global image editor if enabled in the plugin configuration.
13
- * This component should be rendered in your app layout.
14
- *
15
- * @example
16
- * ```tsx
17
- * import { ImagesPluginInit } from '@jhits/plugin-images';
18
- *
19
- * // After calling initImagesPlugin() in a useEffect or script
20
- * <ImagesPluginInit />
21
- * ```
22
- */
23
- export declare function ImagesPluginInit(): import("react/jsx-runtime").JSX.Element;
24
- //# sourceMappingURL=ImagesPluginInit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ImagesPluginInit.d.ts","sourceRoot":"","sources":["ImagesPluginInit.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,4CAE/B"}
@@ -1,28 +0,0 @@
1
- /**
2
- * Images Plugin Initialization Component
3
- *
4
- * This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
5
- * and renders the GlobalImageEditor if enabled.
6
- *
7
- * Render this once in your app layout after calling initImagesPlugin().
8
- */
9
- 'use client';
10
- import { jsx as _jsx } from "react/jsx-runtime";
11
- import { GlobalImageEditor } from './GlobalImageEditor';
12
- /**
13
- * Images Plugin Initialization Component
14
- *
15
- * Renders the global image editor if enabled in the plugin configuration.
16
- * This component should be rendered in your app layout.
17
- *
18
- * @example
19
- * ```tsx
20
- * import { ImagesPluginInit } from '@jhits/plugin-images';
21
- *
22
- * // After calling initImagesPlugin() in a useEffect or script
23
- * <ImagesPluginInit />
24
- * ```
25
- */
26
- export function ImagesPluginInit() {
27
- return _jsx(GlobalImageEditor, {});
28
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * Images Plugin Initialization Component
3
- *
4
- * This component reads from window.__JHITS_PLUGIN_PROPS__['plugin-images']
5
- * and renders the GlobalImageEditor if enabled.
6
- *
7
- * Render this once in your app layout after calling initImagesPlugin().
8
- */
9
-
10
- 'use client';
11
-
12
- import { GlobalImageEditor } from './GlobalImageEditor';
13
-
14
- /**
15
- * Images Plugin Initialization Component
16
- *
17
- * Renders the global image editor if enabled in the plugin configuration.
18
- * This component should be rendered in your app layout.
19
- *
20
- * @example
21
- * ```tsx
22
- * import { ImagesPluginInit } from '@jhits/plugin-images';
23
- *
24
- * // After calling initImagesPlugin() in a useEffect or script
25
- * <ImagesPluginInit />
26
- * ```
27
- */
28
- export function ImagesPluginInit() {
29
- return <GlobalImageEditor />;
30
- }
31
-
@@ -1,10 +0,0 @@
1
- /**
2
- * Image Picker Component Exports
3
- */
4
-
5
- export { ImagePicker } from './ImagePicker';
6
- export { ImageEditor } from './ImageEditor';
7
- export { ImageEffectsPanel } from './ImageEffectsPanel';
8
- export { ImageBrowserModal } from './ImageBrowserModal';
9
- export type { ImagePickerProps } from '../types';
10
-