@jhits/botanics_and_you 0.0.1 → 0.0.3
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.
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/plugin-blog/api-config.d.ts +4 -0
- package/dist/plugin-blog/api-config.d.ts.map +1 -0
- package/dist/plugin-blog/api-config.js +7 -0
- package/dist/plugin-blog/blocks/CategoryDropdown.d.ts +10 -0
- package/dist/plugin-blog/blocks/CategoryDropdown.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/CategoryDropdown.js +67 -0
- package/dist/plugin-blog/blocks/Heading.d.ts +3 -0
- package/dist/plugin-blog/blocks/Heading.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Heading.js +83 -0
- package/dist/plugin-blog/blocks/Hero.d.ts +17 -0
- package/dist/plugin-blog/blocks/Hero.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Hero.js +84 -0
- package/dist/plugin-blog/blocks/Image.d.ts +6 -0
- package/dist/plugin-blog/blocks/Image.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Image.js +285 -0
- package/dist/plugin-blog/blocks/List.d.ts +3 -0
- package/dist/plugin-blog/blocks/List.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/List.js +215 -0
- package/dist/plugin-blog/blocks/Paragraph.d.ts +3 -0
- package/dist/plugin-blog/blocks/Paragraph.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Paragraph.js +64 -0
- package/dist/plugin-blog/blocks/Recipe.d.ts +13 -0
- package/dist/plugin-blog/blocks/Recipe.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Recipe.js +136 -0
- package/dist/plugin-blog/blocks/Table.d.ts +6 -0
- package/dist/plugin-blog/blocks/Table.d.ts.map +1 -0
- package/dist/plugin-blog/blocks/Table.js +115 -0
- package/dist/plugin-blog/blocks.d.ts +14 -0
- package/dist/plugin-blog/blocks.d.ts.map +1 -0
- package/dist/plugin-blog/blocks.js +27 -0
- package/dist/plugin-blog/index.d.ts +17 -0
- package/dist/plugin-blog/index.d.ts.map +1 -0
- package/dist/plugin-blog/index.js +18 -0
- package/dist/plugin-blog/theme.d.ts +19 -0
- package/dist/plugin-blog/theme.d.ts.map +1 -0
- package/dist/plugin-blog/theme.js +28 -0
- package/package.json +27 -17
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { Image as ImageIcon, X, Maximize2, Settings2 } from 'lucide-react';
|
|
6
|
+
import { ImagePicker, Image as PluginImage } from '@jhits/plugin-images';
|
|
7
|
+
import { createApiUrl } from '../api-config';
|
|
8
|
+
const borderRadiusClasses = {
|
|
9
|
+
none: 'rounded-none',
|
|
10
|
+
sm: 'rounded-sm',
|
|
11
|
+
md: 'rounded-md',
|
|
12
|
+
lg: 'rounded-lg',
|
|
13
|
+
xl: 'rounded-xl',
|
|
14
|
+
'2xl': 'rounded-2xl',
|
|
15
|
+
'3xl': 'rounded-3xl',
|
|
16
|
+
full: 'rounded-full',
|
|
17
|
+
};
|
|
18
|
+
export const ImageEdit = ({ block, onUpdate }) => {
|
|
19
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
20
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
21
|
+
const [actualWidth, setActualWidth] = useState(null);
|
|
22
|
+
const [mounted, setMounted] = useState(false);
|
|
23
|
+
// Handle SSR - ensure we only render portal on client
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
}, []);
|
|
27
|
+
const isSavingRef = useRef(false); // Prevent duplicate saves
|
|
28
|
+
const containerRef = useRef(null);
|
|
29
|
+
const wrapperRef = useRef(null);
|
|
30
|
+
// We use a ref to track the latest data without triggering re-renders during drag
|
|
31
|
+
const data = (block.data || {});
|
|
32
|
+
const { imageId, caption = '', alt = '', borderRadius = 'xl', height = 450, widthPercent = 100, brightness = 100, blur = 0, scale = 1.0, positionX = 0, positionY = 0 } = data;
|
|
33
|
+
// MEASUREMENT: Track actual pixel width for the ImagePicker aspect ratio
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!containerRef.current)
|
|
36
|
+
return;
|
|
37
|
+
const obs = new ResizeObserver((entries) => {
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
setActualWidth(entry.contentRect.width);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
obs.observe(containerRef.current);
|
|
43
|
+
return () => obs.disconnect();
|
|
44
|
+
}, []);
|
|
45
|
+
const derivedAspectRatio = useMemo(() => {
|
|
46
|
+
const width = actualWidth || (widthPercent / 100) * 1000;
|
|
47
|
+
return `${Math.round(width)}/${height}`;
|
|
48
|
+
}, [actualWidth, height, widthPercent]);
|
|
49
|
+
// Listen to image-mapping-updated events to sync block data with API
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!imageId)
|
|
52
|
+
return;
|
|
53
|
+
const handleMappingUpdate = (e) => {
|
|
54
|
+
var _a;
|
|
55
|
+
if (((_a = e.detail) === null || _a === void 0 ? void 0 : _a.id) === imageId) {
|
|
56
|
+
// Update block data when ImageEditor saves to API
|
|
57
|
+
const updates = {};
|
|
58
|
+
if (e.detail.scale !== undefined)
|
|
59
|
+
updates.scale = e.detail.scale;
|
|
60
|
+
if (e.detail.positionX !== undefined)
|
|
61
|
+
updates.positionX = e.detail.positionX;
|
|
62
|
+
if (e.detail.positionY !== undefined)
|
|
63
|
+
updates.positionY = e.detail.positionY;
|
|
64
|
+
if (e.detail.brightness !== undefined)
|
|
65
|
+
updates.brightness = e.detail.brightness;
|
|
66
|
+
if (e.detail.blur !== undefined)
|
|
67
|
+
updates.blur = e.detail.blur;
|
|
68
|
+
if (Object.keys(updates).length > 0) {
|
|
69
|
+
// Use the latest block.data to merge updates
|
|
70
|
+
const latestData = (block.data || {});
|
|
71
|
+
onUpdate(Object.assign(Object.assign({}, latestData), updates));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
window.addEventListener('image-mapping-updated', handleMappingUpdate);
|
|
76
|
+
return () => window.removeEventListener('image-mapping-updated', handleMappingUpdate);
|
|
77
|
+
}, [imageId, block.data, onUpdate]);
|
|
78
|
+
const updateField = useCallback((updates) => {
|
|
79
|
+
// Get the latest data by reading from block.data directly (not from closure)
|
|
80
|
+
const latestData = (block.data || {});
|
|
81
|
+
const mergedData = Object.assign(Object.assign({}, latestData), updates);
|
|
82
|
+
onUpdate(mergedData);
|
|
83
|
+
// Note: Scale and position updates are now handled by ImageEditor directly
|
|
84
|
+
// Only save brightness/blur if they changed (ImageEditor handles scale/position)
|
|
85
|
+
if (imageId && (updates.brightness !== undefined || updates.blur !== undefined)) {
|
|
86
|
+
const saveToMapping = async () => {
|
|
87
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
88
|
+
try {
|
|
89
|
+
// Get current filename from the image mapping
|
|
90
|
+
const resolveResponse = await fetch(createApiUrl(`/plugin-images/resolve?id=${encodeURIComponent(imageId)}`));
|
|
91
|
+
const resolved = await resolveResponse.ok ? await resolveResponse.json() : null;
|
|
92
|
+
const filename = (resolved === null || resolved === void 0 ? void 0 : resolved.filename) || imageId;
|
|
93
|
+
// Get current values from API to preserve scale/position
|
|
94
|
+
const currentScale = (_b = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.scale) !== null && _a !== void 0 ? _a : mergedData.scale) !== null && _b !== void 0 ? _b : 1.0;
|
|
95
|
+
const currentPositionX = (_d = (_c = resolved === null || resolved === void 0 ? void 0 : resolved.positionX) !== null && _c !== void 0 ? _c : mergedData.positionX) !== null && _d !== void 0 ? _d : 0;
|
|
96
|
+
const currentPositionY = (_f = (_e = resolved === null || resolved === void 0 ? void 0 : resolved.positionY) !== null && _e !== void 0 ? _e : mergedData.positionY) !== null && _f !== void 0 ? _f : 0;
|
|
97
|
+
const finalValues = {
|
|
98
|
+
brightness: (_g = mergedData.brightness) !== null && _g !== void 0 ? _g : 100,
|
|
99
|
+
blur: (_h = mergedData.blur) !== null && _h !== void 0 ? _h : 0,
|
|
100
|
+
scale: currentScale,
|
|
101
|
+
positionX: currentPositionX,
|
|
102
|
+
positionY: currentPositionY,
|
|
103
|
+
};
|
|
104
|
+
// Save updated values to image mapping
|
|
105
|
+
const response = await fetch(createApiUrl('/plugin-images/resolve'), {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify(Object.assign({ id: imageId, filename }, finalValues)),
|
|
109
|
+
});
|
|
110
|
+
if (response.ok) {
|
|
111
|
+
// Dispatch event to notify Image components to update
|
|
112
|
+
window.dispatchEvent(new CustomEvent('image-mapping-updated', {
|
|
113
|
+
detail: Object.assign({ id: imageId }, finalValues)
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error('[Image Block] Failed to save image mapping:', error);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// Debounce the save to avoid too many API calls
|
|
122
|
+
setTimeout(saveToMapping, 300);
|
|
123
|
+
}
|
|
124
|
+
}, [block.data, onUpdate, imageId]);
|
|
125
|
+
// RESIZING LOGIC (High Performance)
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!isResizing)
|
|
128
|
+
return;
|
|
129
|
+
const handleMouseMove = (e) => {
|
|
130
|
+
if (!containerRef.current || !wrapperRef.current)
|
|
131
|
+
return;
|
|
132
|
+
const wrapperRect = wrapperRef.current.getBoundingClientRect();
|
|
133
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
134
|
+
// Calculate raw values
|
|
135
|
+
const rawHeight = e.clientY - containerRect.top;
|
|
136
|
+
const rawWidthPx = e.clientX - containerRect.left;
|
|
137
|
+
// Snapping Logic
|
|
138
|
+
const snappedHeight = Math.round(rawHeight / 10) * 10;
|
|
139
|
+
const clampedHeight = Math.max(150, Math.min(1200, snappedHeight));
|
|
140
|
+
const calculatedWidthPercent = (rawWidthPx / wrapperRect.width) * 100;
|
|
141
|
+
const snappedWidthPercent = Math.round(calculatedWidthPercent / 2) * 2;
|
|
142
|
+
const clampedWidthPercent = Math.max(15, Math.min(100, snappedWidthPercent));
|
|
143
|
+
// DIRECT DOM MANIPULATION (This is what makes it smooth)
|
|
144
|
+
containerRef.current.style.height = `${clampedHeight}px`;
|
|
145
|
+
containerRef.current.style.width = `${clampedWidthPercent}%`;
|
|
146
|
+
};
|
|
147
|
+
const handleMouseUp = () => {
|
|
148
|
+
if (containerRef.current) {
|
|
149
|
+
// Save the final values to React state only once at the end
|
|
150
|
+
const finalHeight = parseInt(containerRef.current.style.height);
|
|
151
|
+
const finalWidth = parseFloat(containerRef.current.style.width);
|
|
152
|
+
updateField({
|
|
153
|
+
height: finalHeight,
|
|
154
|
+
widthPercent: finalWidth
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
setIsResizing(false);
|
|
158
|
+
};
|
|
159
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
160
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
161
|
+
document.body.style.cursor = 'nwse-resize';
|
|
162
|
+
document.body.style.userSelect = 'none'; // Prevent text selection while dragging
|
|
163
|
+
return () => {
|
|
164
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
165
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
166
|
+
document.body.style.cursor = 'default';
|
|
167
|
+
document.body.style.userSelect = 'auto';
|
|
168
|
+
};
|
|
169
|
+
}, [isResizing, updateField]);
|
|
170
|
+
return (_jsxs("div", { ref: wrapperRef, className: "relative w-full flex flex-col items-center py-8", children: [imageId ? (_jsxs("div", { ref: containerRef, style: { height: `${height}px`, width: `${widthPercent}%`, maxWidth: '100%' }, className: "relative group/image-container touch-none", children: [_jsxs("div", { className: `relative w-full h-full overflow-hidden bg-neutral-100 border border-neutral-200 shadow-sm ${borderRadiusClasses[borderRadius]} group/img`, children: [_jsx("div", { className: "w-full h-full cursor-pointer relative", onClick: () => setShowSettings(true), children: _jsx(PluginImage, Object.assign({ id: imageId, alt: alt || caption, fill: true, className: "object-cover w-full h-full pointer-events-none", editable: false }, {
|
|
171
|
+
scale,
|
|
172
|
+
positionX,
|
|
173
|
+
positionY,
|
|
174
|
+
brightness,
|
|
175
|
+
blur,
|
|
176
|
+
})) }), _jsx("div", { className: "absolute top-3 right-3 opacity-0 group-hover/img:opacity-100 transition-opacity z-40", children: _jsxs("button", { onClick: () => setShowSettings(true), className: "bg-white/95 p-2 rounded-xl shadow-xl border border-neutral-200 text-neutral-800 hover:bg-white flex items-center gap-2 text-[11px] font-bold px-4 uppercase tracking-tight", children: [_jsx(Settings2, { size: 14, className: "text-primary" }), " Settings"] }) }), _jsx("div", { onMouseDown: (e) => { e.preventDefault(); setIsResizing(true); }, className: "absolute bottom-0 right-0 w-8 h-8 cursor-nwse-resize z-50 flex items-center justify-center opacity-0 group-hover/img:opacity-100", children: _jsx("div", { className: "bg-white shadow-lg border border-neutral-200 rounded-tl-lg p-1 text-neutral-600", children: _jsx(Maximize2, { size: 16, className: "rotate-90" }) }) })] }), _jsx("input", { type: "text", value: caption, onChange: (e) => updateField({ caption: e.target.value }), placeholder: "Klik om bijschrift toe te voegen...", className: "w-full mt-4 text-sm text-neutral-400 bg-transparent border-none outline-none focus:ring-0 text-center italic" })] })) : (_jsxs("div", { className: "w-full h-[300px] bg-neutral-50 border-2 border-dashed border-neutral-200 flex flex-col items-center justify-center cursor-pointer hover:bg-primary/5 rounded-3xl", onClick: () => setShowSettings(true), children: [_jsx(ImageIcon, { size: 32, className: "text-neutral-300 mb-2" }), _jsx("p", { className: "text-xs font-bold text-neutral-400 uppercase tracking-widest", children: "Selecteer Afbeelding" })] })), showSettings && mounted && createPortal(_jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 text-left", children: _jsxs("div", { className: "bg-white dark:bg-neutral-900 rounded-3xl w-full max-w-5xl p-6 shadow-2xl max-h-[95vh] flex flex-col border border-neutral-200 dark:border-neutral-800", children: [_jsxs("div", { className: "flex items-center justify-between mb-6", children: [_jsx("h3", { className: "text-xl font-black uppercase tracking-tighter text-neutral-900 dark:text-neutral-100", children: "Configuratie" }), _jsx("button", { onClick: () => setShowSettings(false), className: "p-2 hover:bg-neutral-100 rounded-full transition-colors", children: _jsx(X, { size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsx(ImagePicker, Object.assign({ value: imageId, onChange: (img) => updateField({ imageId: img === null || img === void 0 ? void 0 : img.id }), brightness: brightness, blur: blur }, {
|
|
177
|
+
scale,
|
|
178
|
+
positionX,
|
|
179
|
+
positionY,
|
|
180
|
+
}, {
|
|
181
|
+
// Pass brightness and blur handlers for real-time updates during editing
|
|
182
|
+
// These only update local state - onEditorSave will save everything together
|
|
183
|
+
onBrightnessChange: (val) => {
|
|
184
|
+
// Update local state only - don't trigger save
|
|
185
|
+
const latestData = (block.data || {});
|
|
186
|
+
onUpdate(Object.assign(Object.assign({}, latestData), { brightness: val }));
|
|
187
|
+
}, onBlurChange: (val) => {
|
|
188
|
+
// Update local state only - don't trigger save
|
|
189
|
+
const latestData = (block.data || {});
|
|
190
|
+
onUpdate(Object.assign(Object.assign({}, latestData), { blur: val }));
|
|
191
|
+
},
|
|
192
|
+
// Don't pass scale/position handlers - onEditorSave will save them all together
|
|
193
|
+
onEditorSave: async (finalScale, finalPositionX, finalPositionY, finalBrightness, finalBlur) => {
|
|
194
|
+
if (!imageId || isSavingRef.current)
|
|
195
|
+
return; // Prevent duplicate saves
|
|
196
|
+
isSavingRef.current = true; // Set flag to prevent concurrent saves
|
|
197
|
+
try {
|
|
198
|
+
// Get the actual filename from the API (resolve the semantic ID)
|
|
199
|
+
let filename = imageId; // Fallback to semantic ID
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch(createApiUrl(`/plugin-images/resolve?id=${encodeURIComponent(imageId)}`));
|
|
202
|
+
if (response.ok) {
|
|
203
|
+
const data = await response.json();
|
|
204
|
+
filename = data.filename || imageId;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error('Failed to resolve filename:', error);
|
|
209
|
+
}
|
|
210
|
+
// Normalize position values
|
|
211
|
+
const normalizedPositionX = finalPositionX === -50 ? 0 : finalPositionX;
|
|
212
|
+
const normalizedPositionY = finalPositionY === -50 ? 0 : finalPositionY;
|
|
213
|
+
const finalBrightnessValue = finalBrightness !== null && finalBrightness !== void 0 ? finalBrightness : brightness;
|
|
214
|
+
const finalBlurValue = finalBlur !== null && finalBlur !== void 0 ? finalBlur : blur;
|
|
215
|
+
// Save to plugin-images API (ONLY ONCE)
|
|
216
|
+
const saveData = {
|
|
217
|
+
id: imageId,
|
|
218
|
+
filename: filename,
|
|
219
|
+
scale: finalScale,
|
|
220
|
+
positionX: normalizedPositionX,
|
|
221
|
+
positionY: normalizedPositionY,
|
|
222
|
+
brightness: finalBrightnessValue,
|
|
223
|
+
blur: finalBlurValue,
|
|
224
|
+
};
|
|
225
|
+
const response = await fetch(createApiUrl('/plugin-images/resolve'), {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
body: JSON.stringify(saveData),
|
|
229
|
+
});
|
|
230
|
+
if (response.ok) {
|
|
231
|
+
// Update local block data WITHOUT triggering another save
|
|
232
|
+
// Get latest data and update directly
|
|
233
|
+
const latestData = (block.data || {});
|
|
234
|
+
onUpdate(Object.assign(Object.assign({}, latestData), { scale: finalScale, positionX: normalizedPositionX, positionY: normalizedPositionY, brightness: finalBrightnessValue, blur: finalBlurValue }));
|
|
235
|
+
// Dispatch event to notify Image components
|
|
236
|
+
window.dispatchEvent(new CustomEvent('image-mapping-updated', {
|
|
237
|
+
detail: saveData
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error('Failed to save image transform:', error);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
// Reset flag after a short delay to allow the save to complete
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
isSavingRef.current = false;
|
|
248
|
+
}, 500);
|
|
249
|
+
}
|
|
250
|
+
}, aspectRatio: derivedAspectRatio, borderRadius: borderRadiusClasses[borderRadius] }), `picker-${derivedAspectRatio}`), _jsxs("div", { className: "mt-8 pt-8 border-t border-neutral-100 dark:border-neutral-800 grid grid-cols-1 md:grid-cols-2 gap-6 pb-6", children: [_jsxs("div", { children: [_jsx("label", { className: "text-[10px] font-bold uppercase text-neutral-400 block mb-2", children: "Hoek Afronding" }), _jsx("select", { value: borderRadius, onChange: (e) => updateField({ borderRadius: e.target.value }), className: "w-full p-3 bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm outline-none", children: Object.keys(borderRadiusClasses).map(k => _jsx("option", { value: k, children: k.toUpperCase() }, k)) })] }), _jsxs("div", { children: [_jsx("label", { className: "text-[10px] font-bold uppercase text-neutral-400 block mb-2", children: "Alt Tekst (SEO)" }), _jsx("input", { type: "text", value: alt, onChange: (e) => updateField({ alt: e.target.value }), placeholder: "Beschrijf de afbeelding...", className: "w-full p-3 bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl text-sm outline-none" })] })] })] }), _jsx("div", { className: "mt-4 pt-4 border-t border-neutral-100 dark:border-neutral-800 flex justify-end", children: _jsx("button", { onClick: () => setShowSettings(false), className: "px-12 py-3.5 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 font-bold rounded-2xl shadow-xl hover:opacity-90", children: "Opslaan" }) })] }) }), document.body)] }));
|
|
251
|
+
};
|
|
252
|
+
export const ImagePreview = ({ block }) => {
|
|
253
|
+
const data = (block.data || {});
|
|
254
|
+
const { imageId, caption, alt, borderRadius = 'xl', height = 450, widthPercent = 100, brightness = 100, blur = 0, scale = 1, positionX = 0, positionY = 0 } = data;
|
|
255
|
+
if (!imageId)
|
|
256
|
+
return null;
|
|
257
|
+
return (_jsxs("figure", { className: "w-full flex flex-col items-center my-14", children: [_jsx("div", { style: { height: `${height}px`, width: `${widthPercent}%`, maxWidth: '100%' }, className: `relative overflow-hidden shadow-sm ${borderRadiusClasses[borderRadius]}`, children: _jsx(PluginImage, Object.assign({ id: imageId, alt: alt || caption || '', fill: true, className: "object-cover w-full h-full", editable: false }, {
|
|
258
|
+
scale,
|
|
259
|
+
positionX,
|
|
260
|
+
positionY,
|
|
261
|
+
brightness,
|
|
262
|
+
blur,
|
|
263
|
+
})) }), caption && _jsx("figcaption", { className: "mt-5 text-center text-neutral-500 text-sm italic max-w-[80%]", children: caption })] }));
|
|
264
|
+
};
|
|
265
|
+
export const imageBlock = {
|
|
266
|
+
type: 'image',
|
|
267
|
+
name: 'Afbeelding',
|
|
268
|
+
description: 'Resizable afbeelding met geavanceerde filters en synchronisatie',
|
|
269
|
+
icon: ImageIcon,
|
|
270
|
+
category: 'media',
|
|
271
|
+
defaultData: {
|
|
272
|
+
imageId: undefined,
|
|
273
|
+
caption: '',
|
|
274
|
+
alt: '',
|
|
275
|
+
borderRadius: 'xl',
|
|
276
|
+
height: 450,
|
|
277
|
+
widthPercent: 100,
|
|
278
|
+
brightness: 100,
|
|
279
|
+
blur: 0,
|
|
280
|
+
scale: 1,
|
|
281
|
+
positionX: 0,
|
|
282
|
+
positionY: 0
|
|
283
|
+
},
|
|
284
|
+
components: { Edit: ImageEdit, Preview: ImagePreview, Icon: ImageIcon },
|
|
285
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"List.d.ts","sourceRoot":"","sources":["../../../src/plugin-blog/blocks/List.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqC,qBAAqB,EAA6D,MAAM,oBAAoB,CAAC;AAsRzJ,eAAO,MAAM,SAAS,EAAE,qBA0BvB,CAAC"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RichTextEditor, RichTextPreview } from "@jhits/plugin-blog";
|
|
3
|
+
import { ListChecks } from "lucide-react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
/**
|
|
6
|
+
* Rich Text Formatting Configuration for List Items
|
|
7
|
+
* Matches BotanicsAndYou's design system
|
|
8
|
+
*/
|
|
9
|
+
const listFormatting = {
|
|
10
|
+
bold: true,
|
|
11
|
+
italic: true,
|
|
12
|
+
underline: true,
|
|
13
|
+
links: true,
|
|
14
|
+
colors: ['text-forest', 'text-sage', 'text-primary'],
|
|
15
|
+
styles: {
|
|
16
|
+
bold: 'font-bold text-black',
|
|
17
|
+
italic: 'italic',
|
|
18
|
+
underline: 'underline decoration-primary/30',
|
|
19
|
+
link: 'text-primary underline decoration-primary/50 hover:text-primary/80 hover:decoration-primary/70 transition-colors',
|
|
20
|
+
colorClasses: {
|
|
21
|
+
'text-forest': 'text-forest',
|
|
22
|
+
'text-sage': 'text-sage',
|
|
23
|
+
'text-primary': 'text-primary',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* 1. Shared UI Wrapper
|
|
29
|
+
*/
|
|
30
|
+
const ListContainer = ({ children, type }) => {
|
|
31
|
+
// Determine the tailwind class for bullet styling
|
|
32
|
+
const getListClass = () => {
|
|
33
|
+
if (type === "ol")
|
|
34
|
+
return "list-decimal pl-5";
|
|
35
|
+
if (type === "checklist")
|
|
36
|
+
return "list-none pl-0";
|
|
37
|
+
return "list-disc pl-5"; // default ul
|
|
38
|
+
};
|
|
39
|
+
return (_jsx("div", { className: "bg-white rounded-2xl border border-gray-100 shadow-sm relative", children: _jsx("div", { className: "px-4 pb-4 mt-4", children: _jsx("ul", { className: `${getListClass()} space-y-3 pr-4 text-gray-800`, children: children }) }) }));
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 2. Editor Component
|
|
43
|
+
*/
|
|
44
|
+
const ListEdit = ({ block, onUpdate }) => {
|
|
45
|
+
// Ensure items are treated consistently as ItemObj[]
|
|
46
|
+
// Handle both string[] (legacy) and ItemObj[] formats
|
|
47
|
+
const containerRef = React.useRef(null);
|
|
48
|
+
const rawItems = Array.isArray(block.data.items) ? block.data.items : [];
|
|
49
|
+
const items = rawItems.map((item) => {
|
|
50
|
+
if (typeof item === 'string') {
|
|
51
|
+
return { text: item, html: item };
|
|
52
|
+
}
|
|
53
|
+
if (typeof item === 'object' && item !== null && 'text' in item) {
|
|
54
|
+
const itemObj = item;
|
|
55
|
+
return {
|
|
56
|
+
text: itemObj.text || '',
|
|
57
|
+
html: itemObj.html || itemObj.text || ''
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { text: '', html: '' };
|
|
61
|
+
});
|
|
62
|
+
const type = (block.data.type || "ul");
|
|
63
|
+
const handleAddItem = (index) => {
|
|
64
|
+
const newItems = [...items];
|
|
65
|
+
newItems.splice(index + 1, 0, { text: '', html: '' });
|
|
66
|
+
onUpdate({ items: newItems });
|
|
67
|
+
};
|
|
68
|
+
return (_jsx("div", { ref: containerRef, children: _jsx(ListContainer, { type: type, children: items.map((item, idx) => {
|
|
69
|
+
const html = item.html || item.text || '';
|
|
70
|
+
const isEmpty = !html || html.replace(/<[^>]*>/g, '').trim() === '';
|
|
71
|
+
return (_jsxs("li", { className: `${type === "checklist" ? "flex items-start gap-3" : ""}`, children: [type === "checklist" && (_jsx("input", { type: "checkbox", disabled: true, className: "mt-1.5 w-4 h-4 accent-gray-400 cursor-not-allowed shrink-0" })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(RichTextEditor, { value: html, onChange: (htmlContent) => {
|
|
72
|
+
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
73
|
+
const newItems = [...items];
|
|
74
|
+
newItems[idx] = {
|
|
75
|
+
text: textContent,
|
|
76
|
+
html: htmlContent
|
|
77
|
+
};
|
|
78
|
+
onUpdate({ items: newItems });
|
|
79
|
+
}, onKeyDown: (e) => {
|
|
80
|
+
const contentEditable = e.target;
|
|
81
|
+
if (!contentEditable || contentEditable.contentEditable !== 'true')
|
|
82
|
+
return;
|
|
83
|
+
const currentHtml = contentEditable.innerHTML || '';
|
|
84
|
+
const currentText = currentHtml.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();
|
|
85
|
+
const isEmpty = !currentText || currentText === '';
|
|
86
|
+
// Intercept Enter key to create new list item instead of line break
|
|
87
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
// Update current item with trimmed content
|
|
91
|
+
const newItems = [...items];
|
|
92
|
+
newItems[idx] = {
|
|
93
|
+
text: currentText,
|
|
94
|
+
html: currentText
|
|
95
|
+
};
|
|
96
|
+
// Add new item after current one
|
|
97
|
+
const newItem = { text: '', html: '' };
|
|
98
|
+
newItems.splice(idx + 1, 0, newItem);
|
|
99
|
+
onUpdate({ items: newItems });
|
|
100
|
+
// Focus the next editor after re-render
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
// Find all contentEditable elements and get the next one
|
|
103
|
+
const allEditors = document.querySelectorAll('[contenteditable="true"]');
|
|
104
|
+
const currentEditorIndex = Array.from(allEditors).indexOf(contentEditable);
|
|
105
|
+
const nextEditor = allEditors[currentEditorIndex + 1];
|
|
106
|
+
if (nextEditor) {
|
|
107
|
+
nextEditor.focus();
|
|
108
|
+
// Place cursor at the start
|
|
109
|
+
const range = document.createRange();
|
|
110
|
+
range.selectNodeContents(nextEditor);
|
|
111
|
+
range.collapse(true);
|
|
112
|
+
const selection = window.getSelection();
|
|
113
|
+
selection === null || selection === void 0 ? void 0 : selection.removeAllRanges();
|
|
114
|
+
selection === null || selection === void 0 ? void 0 : selection.addRange(range);
|
|
115
|
+
}
|
|
116
|
+
}, 0);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Handle Backspace/Delete to remove empty list items
|
|
120
|
+
if ((e.key === 'Backspace' || e.key === 'Delete') && isEmpty && items.length > 1) {
|
|
121
|
+
const selection = window.getSelection();
|
|
122
|
+
if (!selection || selection.rangeCount === 0)
|
|
123
|
+
return;
|
|
124
|
+
const range = selection.getRangeAt(0);
|
|
125
|
+
const isAtStart = () => {
|
|
126
|
+
if (!range.collapsed)
|
|
127
|
+
return false;
|
|
128
|
+
const startRange = range.cloneRange();
|
|
129
|
+
startRange.selectNodeContents(contentEditable);
|
|
130
|
+
startRange.setEnd(range.startContainer, range.startOffset);
|
|
131
|
+
return startRange.toString().trim() === '';
|
|
132
|
+
};
|
|
133
|
+
if ((e.key === 'Backspace' && isAtStart())) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
// Calculate focus index BEFORE updating state
|
|
137
|
+
const focusIndex = idx > 0 ? idx - 1 : 0;
|
|
138
|
+
const newItems = items.filter((_, i) => i !== idx);
|
|
139
|
+
onUpdate({ items: newItems });
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
// SCOPED SEARCH: Look only inside this specific list container
|
|
142
|
+
if (containerRef.current) {
|
|
143
|
+
const listEditors = containerRef.current.querySelectorAll('[contenteditable="true"]');
|
|
144
|
+
const targetEditor = listEditors[focusIndex];
|
|
145
|
+
if (targetEditor) {
|
|
146
|
+
targetEditor.focus();
|
|
147
|
+
const newRange = document.createRange();
|
|
148
|
+
newRange.selectNodeContents(targetEditor);
|
|
149
|
+
newRange.collapse(false); // Go to end of previous item
|
|
150
|
+
const newSel = window.getSelection();
|
|
151
|
+
newSel === null || newSel === void 0 ? void 0 : newSel.removeAllRanges();
|
|
152
|
+
newSel === null || newSel === void 0 ? void 0 : newSel.addRange(newRange);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}, 0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, placeholder: "Typ een item...", formatting: listFormatting, className: "leading-relaxed min-h-[1.5em] w-full text-gray-800", isFocused: false }), isEmpty && idx === items.length - 1 && (_jsx("button", { type: "button", onClick: () => handleAddItem(idx), className: "mt-2 text-xs text-gray-400 hover:text-gray-600", children: "+ Voeg item toe" }))] })] }, idx));
|
|
159
|
+
}) }) }));
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* 3. Preview Component
|
|
163
|
+
*/
|
|
164
|
+
const ListPreview = ({ block }) => {
|
|
165
|
+
// Handle both string[] (legacy) and ItemObj[] formats
|
|
166
|
+
const rawItems = Array.isArray(block.data.items) ? block.data.items : [];
|
|
167
|
+
const items = rawItems.map((item) => {
|
|
168
|
+
if (typeof item === 'string') {
|
|
169
|
+
return { text: item, html: item };
|
|
170
|
+
}
|
|
171
|
+
if (typeof item === 'object' && item !== null && 'text' in item) {
|
|
172
|
+
const itemObj = item;
|
|
173
|
+
return {
|
|
174
|
+
text: itemObj.text || '',
|
|
175
|
+
html: itemObj.html || itemObj.text || ''
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { text: '', html: '' };
|
|
179
|
+
});
|
|
180
|
+
const type = (block.data.type || "ul");
|
|
181
|
+
return (_jsx(ListContainer, { type: type, children: items.map((item, idx) => {
|
|
182
|
+
const html = item.html || item.text || '';
|
|
183
|
+
if (!html || html.replace(/<[^>]*>/g, '').trim() === '')
|
|
184
|
+
return null;
|
|
185
|
+
return (_jsxs("li", { className: `${type === "checklist" ? "flex items-start gap-3" : ""}`, children: [type === "checklist" && (_jsx("input", { type: "checkbox", className: "mt-1.5 w-4 h-4 accent-gray-600 shrink-0" })), _jsx("div", { className: "leading-relaxed min-h-[1.5em] w-full", children: _jsx(RichTextPreview, { content: html, formatting: listFormatting, className: "text-gray-800" }) })] }, idx));
|
|
186
|
+
}) }));
|
|
187
|
+
};
|
|
188
|
+
export const listBlock = {
|
|
189
|
+
type: 'list',
|
|
190
|
+
name: 'Lijst',
|
|
191
|
+
description: '',
|
|
192
|
+
icon: ListChecks,
|
|
193
|
+
defaultData: {
|
|
194
|
+
title: '',
|
|
195
|
+
items: [{ text: '' }],
|
|
196
|
+
},
|
|
197
|
+
category: 'text',
|
|
198
|
+
validate: (data) => {
|
|
199
|
+
return typeof data.title === 'string' &&
|
|
200
|
+
Array.isArray(data.items) &&
|
|
201
|
+
data.items.every((item) => {
|
|
202
|
+
if (typeof item === 'string')
|
|
203
|
+
return true;
|
|
204
|
+
if (typeof item === 'object' && item !== null && 'text' in item) {
|
|
205
|
+
return typeof item.text === 'string';
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
components: {
|
|
211
|
+
Edit: ListEdit,
|
|
212
|
+
Preview: ListPreview,
|
|
213
|
+
Icon: ListChecks,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Paragraph.d.ts","sourceRoot":"","sources":["../../../src/plugin-blog/blocks/Paragraph.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqC,qBAAqB,EAA6D,MAAM,oBAAoB,CAAC;AAkEzJ,eAAO,MAAM,cAAc,EAAE,qBAkB5B,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RichTextEditor, RichTextPreview } from "@jhits/plugin-blog";
|
|
3
|
+
import { Type } from "lucide-react";
|
|
4
|
+
/**
|
|
5
|
+
* Rich Text Formatting Configuration for Paragraph
|
|
6
|
+
* Matches BotanicsAndYou's design system with forest, sage, and primary colors
|
|
7
|
+
* Only enable formatting options that have proper styles defined
|
|
8
|
+
*/
|
|
9
|
+
const paragraphFormatting = {
|
|
10
|
+
bold: true,
|
|
11
|
+
italic: true,
|
|
12
|
+
underline: true,
|
|
13
|
+
links: true,
|
|
14
|
+
colors: ['text-forest', 'text-sage', 'text-primary'],
|
|
15
|
+
styles: {
|
|
16
|
+
// Bold: Use black color for blog bold text
|
|
17
|
+
bold: 'font-bold text-black',
|
|
18
|
+
// Italic: Just italic, no color override
|
|
19
|
+
italic: 'italic',
|
|
20
|
+
// Underline: Simple underline with client's primary color accent
|
|
21
|
+
underline: 'underline decoration-primary/30',
|
|
22
|
+
// Links: Primary color with underline and hover effect
|
|
23
|
+
link: 'text-primary underline decoration-primary/50 hover:text-primary/80 hover:decoration-primary/70 transition-colors',
|
|
24
|
+
// Color classes: Map to client's design system colors
|
|
25
|
+
colorClasses: {
|
|
26
|
+
'text-forest': 'text-forest',
|
|
27
|
+
'text-sage': 'text-sage',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Paragraph Block
|
|
33
|
+
* Rich text paragraph block for botanical articles
|
|
34
|
+
*/
|
|
35
|
+
const ParagraphEdit = ({ block, onUpdate, isSelected }) => {
|
|
36
|
+
const html = block.data.html || block.data.text || '';
|
|
37
|
+
return (_jsx(RichTextEditor, { value: html, onChange: (htmlContent) => {
|
|
38
|
+
// Store both HTML and plain text for backwards compatibility
|
|
39
|
+
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
40
|
+
onUpdate({ html: htmlContent, text: textContent });
|
|
41
|
+
}, placeholder: "Enter paragraph text...", formatting: paragraphFormatting, className: "text-md leading-relaxed text-gray-800 w-full min-h-[20px]", isFocused: isSelected }));
|
|
42
|
+
};
|
|
43
|
+
const ParagraphPreview = ({ block }) => {
|
|
44
|
+
const html = block.data.html || block.data.text || '';
|
|
45
|
+
return (_jsx(RichTextPreview, { content: html, formatting: paragraphFormatting, className: "text-md leading-relaxed text-gray-800 py-4" }));
|
|
46
|
+
};
|
|
47
|
+
export const paragraphBlock = {
|
|
48
|
+
type: 'paragraph',
|
|
49
|
+
name: 'Paragraph',
|
|
50
|
+
description: 'Add a text paragraph',
|
|
51
|
+
icon: Type,
|
|
52
|
+
defaultData: {
|
|
53
|
+
text: '',
|
|
54
|
+
html: '',
|
|
55
|
+
},
|
|
56
|
+
category: 'text',
|
|
57
|
+
validate: (data) => (typeof data.text === 'string' || typeof data.html === 'string'),
|
|
58
|
+
components: {
|
|
59
|
+
Edit: ParagraphEdit,
|
|
60
|
+
Preview: ParagraphPreview,
|
|
61
|
+
Icon: Type,
|
|
62
|
+
},
|
|
63
|
+
richTextFormatting: paragraphFormatting,
|
|
64
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BlockEditProps, BlockPreviewProps, ClientBlockDefinition } from "@jhits/plugin-blog";
|
|
2
|
+
/**
|
|
3
|
+
* 2. Editor Component
|
|
4
|
+
* Handles dynamic list management, keyboard navigation, and auto-focus
|
|
5
|
+
*/
|
|
6
|
+
export declare const RecipeEdit: React.FC<BlockEditProps>;
|
|
7
|
+
/**
|
|
8
|
+
* 3. Preview Component
|
|
9
|
+
* Handles interactive progress tracking for the end user
|
|
10
|
+
*/
|
|
11
|
+
export declare const RecipePreview: React.FC<BlockPreviewProps>;
|
|
12
|
+
export declare const recipeBlock: ClientBlockDefinition;
|
|
13
|
+
//# sourceMappingURL=Recipe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Recipe.d.ts","sourceRoot":"","sources":["../../../src/plugin-blog/blocks/Recipe.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAyC9F;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA+E/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAwErD,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,qBAoBzB,CAAC"}
|