@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.
Files changed (41) hide show
  1. package/dist/index.d.ts +10 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +9 -0
  4. package/dist/plugin-blog/api-config.d.ts +4 -0
  5. package/dist/plugin-blog/api-config.d.ts.map +1 -0
  6. package/dist/plugin-blog/api-config.js +7 -0
  7. package/dist/plugin-blog/blocks/CategoryDropdown.d.ts +10 -0
  8. package/dist/plugin-blog/blocks/CategoryDropdown.d.ts.map +1 -0
  9. package/dist/plugin-blog/blocks/CategoryDropdown.js +67 -0
  10. package/dist/plugin-blog/blocks/Heading.d.ts +3 -0
  11. package/dist/plugin-blog/blocks/Heading.d.ts.map +1 -0
  12. package/dist/plugin-blog/blocks/Heading.js +83 -0
  13. package/dist/plugin-blog/blocks/Hero.d.ts +17 -0
  14. package/dist/plugin-blog/blocks/Hero.d.ts.map +1 -0
  15. package/dist/plugin-blog/blocks/Hero.js +84 -0
  16. package/dist/plugin-blog/blocks/Image.d.ts +6 -0
  17. package/dist/plugin-blog/blocks/Image.d.ts.map +1 -0
  18. package/dist/plugin-blog/blocks/Image.js +285 -0
  19. package/dist/plugin-blog/blocks/List.d.ts +3 -0
  20. package/dist/plugin-blog/blocks/List.d.ts.map +1 -0
  21. package/dist/plugin-blog/blocks/List.js +215 -0
  22. package/dist/plugin-blog/blocks/Paragraph.d.ts +3 -0
  23. package/dist/plugin-blog/blocks/Paragraph.d.ts.map +1 -0
  24. package/dist/plugin-blog/blocks/Paragraph.js +64 -0
  25. package/dist/plugin-blog/blocks/Recipe.d.ts +13 -0
  26. package/dist/plugin-blog/blocks/Recipe.d.ts.map +1 -0
  27. package/dist/plugin-blog/blocks/Recipe.js +136 -0
  28. package/dist/plugin-blog/blocks/Table.d.ts +6 -0
  29. package/dist/plugin-blog/blocks/Table.d.ts.map +1 -0
  30. package/dist/plugin-blog/blocks/Table.js +115 -0
  31. package/dist/plugin-blog/blocks.d.ts +14 -0
  32. package/dist/plugin-blog/blocks.d.ts.map +1 -0
  33. package/dist/plugin-blog/blocks.js +27 -0
  34. package/dist/plugin-blog/index.d.ts +17 -0
  35. package/dist/plugin-blog/index.d.ts.map +1 -0
  36. package/dist/plugin-blog/index.js +18 -0
  37. package/dist/plugin-blog/theme.d.ts +19 -0
  38. package/dist/plugin-blog/theme.d.ts.map +1 -0
  39. package/dist/plugin-blog/theme.js +28 -0
  40. package/package.json +27 -17
  41. 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,3 @@
1
+ import { ClientBlockDefinition } from "@jhits/plugin-blog";
2
+ export declare const listBlock: ClientBlockDefinition;
3
+ //# sourceMappingURL=List.d.ts.map
@@ -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(/&nbsp;/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(/&nbsp;/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,3 @@
1
+ import { ClientBlockDefinition } from "@jhits/plugin-blog";
2
+ export declare const paragraphBlock: ClientBlockDefinition;
3
+ //# sourceMappingURL=Paragraph.d.ts.map
@@ -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(/&nbsp;/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"}