@evolution-james/evolution-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1665 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+
3
+ function _extends() {
4
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
5
+ for (var e = 1; e < arguments.length; e++) {
6
+ var t = arguments[e];
7
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
8
+ }
9
+ return n;
10
+ }, _extends.apply(null, arguments);
11
+ }
12
+
13
+ const PADDING_OPTIONS = ['0px', '4px', '8px', '12px', '16px', '24px', '32px', '40px', '48px', '64px'];
14
+ const MARGIN_OPTIONS = ['0px', '4px', '8px', '12px', '16px', '24px', '32px', '40px', '48px', '64px'];
15
+ /**
16
+ * Block type identifiers used throughout the editor.
17
+ * Each type corresponds to a React block component.
18
+ */
19
+ const BLOCK_TYPES = {
20
+ PARAGRAPH: 'paragraph',
21
+ // Standard text block
22
+ HEADING: 'heading',
23
+ // Heading block (h1, h2, h3)
24
+ LIST: 'list',
25
+ // Ordered or unordered list
26
+ IMAGE: 'image',
27
+ // Image block
28
+ YOUTUBE: 'youtube',
29
+ // Embedded YouTube video
30
+ DIVIDER: 'divider' // Horizontal divider
31
+ };
32
+
33
+ /**
34
+ * Supported heading levels for heading blocks.
35
+ */
36
+ const HEADING_LEVELS = {
37
+ H1: 1,
38
+ H2: 2,
39
+ H3: 3
40
+ };
41
+
42
+ /**
43
+ * List style options for list blocks.
44
+ */
45
+ const LIST_STYLES = {
46
+ UNORDERED: 'unordered',
47
+ // Bulleted list
48
+ ORDERED: 'ordered' // Numbered list
49
+ };
50
+
51
+ /**
52
+ * Default style objects for each block type.
53
+ * These are used as the initial styles for new blocks.
54
+ */
55
+ const DEFAULT_STYLES = {
56
+ heading: {
57
+ 1: {
58
+ fontSize: '32px',
59
+ marginTop: '0px',
60
+ marginBottom: '16px',
61
+ marginLeft: '0px',
62
+ marginRight: '0px',
63
+ paddingTop: '0px',
64
+ paddingBottom: '0px',
65
+ paddingLeft: '0px',
66
+ paddingRight: '0px',
67
+ color: '#000000'
68
+ },
69
+ 2: {
70
+ fontSize: '24px',
71
+ marginTop: '0px',
72
+ marginBottom: '14px',
73
+ marginLeft: '0px',
74
+ marginRight: '0px',
75
+ paddingTop: '0px',
76
+ paddingBottom: '0px',
77
+ paddingLeft: '0px',
78
+ paddingRight: '0px',
79
+ color: '#000000'
80
+ },
81
+ 3: {
82
+ fontSize: '20px',
83
+ marginTop: '0px',
84
+ marginBottom: '12px',
85
+ marginLeft: '0px',
86
+ marginRight: '0px',
87
+ paddingTop: '0px',
88
+ paddingBottom: '0px',
89
+ paddingLeft: '0px',
90
+ paddingRight: '0px',
91
+ color: '#000000'
92
+ }
93
+ },
94
+ paragraph: {
95
+ fontSize: '16px',
96
+ marginTop: '0px',
97
+ marginBottom: '12px',
98
+ marginLeft: '0px',
99
+ marginRight: '0px',
100
+ paddingTop: '0px',
101
+ paddingBottom: '0px',
102
+ paddingLeft: '0px',
103
+ paddingRight: '0px',
104
+ color: '#333333'
105
+ },
106
+ list: {
107
+ fontSize: '16px',
108
+ marginTop: '0px',
109
+ marginBottom: '12px',
110
+ marginLeft: '0px',
111
+ marginRight: '0px',
112
+ paddingTop: '0px',
113
+ paddingBottom: '0px',
114
+ paddingLeft: '40px',
115
+ paddingRight: '0px',
116
+ color: '#333333'
117
+ },
118
+ image: {
119
+ marginTop: '16px',
120
+ marginBottom: '16px',
121
+ marginLeft: '0px',
122
+ marginRight: '0px',
123
+ paddingTop: '0px',
124
+ paddingBottom: '0px',
125
+ paddingLeft: '0px',
126
+ paddingRight: '0px'
127
+ },
128
+ youtube: {
129
+ marginTop: '16px',
130
+ marginBottom: '16px',
131
+ marginLeft: '0px',
132
+ marginRight: '0px',
133
+ paddingTop: '0px',
134
+ paddingBottom: '0px',
135
+ paddingLeft: '0px',
136
+ paddingRight: '0px'
137
+ },
138
+ divider: {
139
+ marginTop: '24px',
140
+ marginBottom: '24px',
141
+ marginLeft: '0px',
142
+ marginRight: '0px',
143
+ paddingTop: '0px',
144
+ paddingBottom: '0px',
145
+ paddingLeft: '0px',
146
+ paddingRight: '0px'
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Font size options for the toolbar font size picker.
152
+ */
153
+ const FONT_SIZE_OPTIONS = ['12px', '14px', '16px', '18px', '20px', '24px', '28px', '32px', '36px', '48px'];
154
+
155
+ /**
156
+ * Color options for the toolbar color pickers (text, border, background).
157
+ */
158
+ const COLOR_OPTIONS = ['#000000', '#333333', '#666666', '#999999', '#CCCCCC', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#800080', '#008000'];
159
+
160
+ /**
161
+ * Generate a unique block ID
162
+ */
163
+ function generateId() {
164
+ return `block_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
165
+ }
166
+
167
+ /**
168
+ * Create an empty paragraph block with instructions
169
+ */
170
+ function createEmptyBlock() {
171
+ return createBlock(BLOCK_TYPES.PARAGRAPH, {
172
+ text: '',
173
+ htmlTag: 'p',
174
+ styles: {
175
+ ...DEFAULT_STYLES.paragraph
176
+ }
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Create a new block with given type and data
182
+ */
183
+ function createBlock(type, data) {
184
+ return {
185
+ id: generateId(),
186
+ type,
187
+ data
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Create a heading block
193
+ */
194
+ function createHeadingBlock(level = HEADING_LEVELS.H1, text = '') {
195
+ return createBlock(BLOCK_TYPES.HEADING, {
196
+ level,
197
+ text,
198
+ htmlTag: `h${level}`,
199
+ styles: {
200
+ ...DEFAULT_STYLES.heading[level]
201
+ }
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Create a list block
207
+ */
208
+ function createListBlock(style = LIST_STYLES.UNORDERED, items = ['']) {
209
+ return createBlock(BLOCK_TYPES.LIST, {
210
+ style,
211
+ items,
212
+ htmlTag: style === LIST_STYLES.UNORDERED ? 'ul' : 'ol',
213
+ styles: {
214
+ ...DEFAULT_STYLES.list
215
+ }
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Create an image block
221
+ */
222
+ function createImageBlock(url = '', alt = '') {
223
+ return createBlock(BLOCK_TYPES.IMAGE, {
224
+ url,
225
+ alt,
226
+ htmlTag: 'img',
227
+ styles: {
228
+ ...DEFAULT_STYLES.image
229
+ }
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Create a YouTube block
235
+ */
236
+ function createYouTubeBlock(url = '') {
237
+ const videoId = extractYouTubeId(url);
238
+ return createBlock(BLOCK_TYPES.YOUTUBE, {
239
+ url,
240
+ videoId,
241
+ htmlTag: 'iframe',
242
+ styles: {
243
+ ...DEFAULT_STYLES.youtube
244
+ }
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Create a divider block
250
+ */
251
+ function createDividerBlock() {
252
+ return createBlock(BLOCK_TYPES.DIVIDER, {
253
+ htmlTag: 'hr',
254
+ styles: {
255
+ ...DEFAULT_STYLES.divider
256
+ }
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Extract YouTube video ID from URL
262
+ */
263
+ function extractYouTubeId(url) {
264
+ if (!url) return '';
265
+ const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
266
+ const match = url.match(regExp);
267
+ return match && match[7].length === 11 ? match[7] : '';
268
+ }
269
+
270
+ /**
271
+ * Update block styles
272
+ */
273
+ function updateBlockStyles(block, styleUpdates) {
274
+ return {
275
+ ...block,
276
+ data: {
277
+ ...block.data,
278
+ styles: {
279
+ ...block.data.styles,
280
+ ...styleUpdates
281
+ }
282
+ }
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Update block data
288
+ */
289
+ function updateBlockData(block, dataUpdates) {
290
+ return {
291
+ ...block,
292
+ data: {
293
+ ...block.data,
294
+ ...dataUpdates
295
+ }
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Serialize editor blocks to JSON format
301
+ */
302
+ function serialize(blocks) {
303
+ return {
304
+ version: '1.0.0',
305
+ time: Date.now(),
306
+ blocks: blocks.map(block => ({
307
+ id: block.id,
308
+ type: block.type,
309
+ data: block.data
310
+ }))
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Deserialize JSON to editor blocks
316
+ */
317
+ function deserialize(json) {
318
+ // Handle empty or invalid JSON
319
+ if (!json || !json.blocks || !Array.isArray(json.blocks)) {
320
+ return [createEmptyBlock()];
321
+ }
322
+
323
+ // Handle empty blocks array
324
+ if (json.blocks.length === 0) {
325
+ return [createEmptyBlock()];
326
+ }
327
+
328
+ // Map JSON blocks to editor blocks
329
+ return json.blocks.map(block => ({
330
+ id: block.id,
331
+ type: block.type,
332
+ data: block.data
333
+ }));
334
+ }
335
+
336
+ /**
337
+ * Main hook for managing editor state
338
+ */
339
+ function useEditor(initialData) {
340
+ const [blocks, setBlocks] = useState(() => {
341
+ if (initialData) {
342
+ return deserialize(initialData);
343
+ }
344
+ return [];
345
+ });
346
+ const [focusedBlockId, setFocusedBlockId] = useState(null);
347
+
348
+ // Update blocks when initialData changes
349
+ useEffect(() => {
350
+ if (initialData) {
351
+ setBlocks(deserialize(initialData));
352
+ } else {
353
+ setBlocks([]);
354
+ }
355
+ }, [initialData]);
356
+
357
+ /**
358
+ * Add a new block at the specified index
359
+ */
360
+ const addBlock = useCallback((block, index) => {
361
+ setBlocks(prev => {
362
+ const newBlocks = [...prev];
363
+ newBlocks.splice(index, 0, block);
364
+ return newBlocks;
365
+ });
366
+ }, []);
367
+
368
+ /**
369
+ * Update a block by ID
370
+ */
371
+ const updateBlock = useCallback((blockId, updates) => {
372
+ setBlocks(prev => prev.map(block => block.id === blockId ? updateBlockData(block, updates) : block));
373
+ }, []);
374
+
375
+ /**
376
+ * Update block styles by ID
377
+ */
378
+ const updateBlockStyle = useCallback((blockId, styleUpdates) => {
379
+ setBlocks(prev => prev.map(block => block.id === blockId ? updateBlockStyles(block, styleUpdates) : block));
380
+ }, []);
381
+
382
+ /**
383
+ * Delete a block by ID
384
+ */
385
+ const deleteBlock = useCallback(blockId => {
386
+ setBlocks(prev => {
387
+ const filtered = prev.filter(block => block.id !== blockId);
388
+ return filtered;
389
+ });
390
+ }, []);
391
+
392
+ /**
393
+ * Move a block to a new position
394
+ */
395
+ const moveBlock = useCallback((fromIndex, toIndex) => {
396
+ setBlocks(prev => {
397
+ const newBlocks = [...prev];
398
+ const [movedBlock] = newBlocks.splice(fromIndex, 1);
399
+ newBlocks.splice(toIndex, 0, movedBlock);
400
+ return newBlocks;
401
+ });
402
+ }, []);
403
+
404
+ /**
405
+ * Get the current editor data as JSON
406
+ */
407
+ const getJSON = useCallback(() => {
408
+ return serialize(blocks);
409
+ }, [blocks]);
410
+
411
+ /**
412
+ * Clear all blocks
413
+ */
414
+ const clear = useCallback(() => {
415
+ setBlocks([createEmptyBlock()]);
416
+ }, []);
417
+ return {
418
+ blocks,
419
+ focusedBlockId,
420
+ setFocusedBlockId,
421
+ addBlock,
422
+ updateBlock,
423
+ updateBlockStyle,
424
+ deleteBlock,
425
+ moveBlock,
426
+ getJSON,
427
+ clear
428
+ };
429
+ }
430
+
431
+ function ToolbarButton({
432
+ onClick,
433
+ children,
434
+ title,
435
+ active,
436
+ disabled
437
+ }) {
438
+ return /*#__PURE__*/React.createElement("button", {
439
+ onClick: onClick,
440
+ title: title,
441
+ disabled: disabled,
442
+ className: `toolbar-button ${active ? 'active' : ''}`,
443
+ type: "button"
444
+ }, children);
445
+ }
446
+
447
+ const TEXT_TYPES = ['paragraph', 'heading', 'list'];
448
+ function Toolbar({
449
+ focusedBlock,
450
+ onUpdateBlockStyle,
451
+ onMoveBlockUp,
452
+ onMoveBlockDown,
453
+ onSave
454
+ }) {
455
+ const [showFontSizePicker, setShowFontSizePicker] = useState(false);
456
+ const [showBgColorPicker, setShowBgColorPicker] = useState(false);
457
+ const [showPaddingPicker, setShowPaddingPicker] = useState(false);
458
+ const [showMarginPicker, setShowMarginPicker] = useState(false);
459
+ const hasFocusedBlock = !!focusedBlock;
460
+ const isTextBlock = focusedBlock && TEXT_TYPES.includes(focusedBlock.type);
461
+ const styles = focusedBlock?.data?.styles || {};
462
+ const handleStyleChange = updates => {
463
+ if (!hasFocusedBlock) return;
464
+ onUpdateBlockStyle(updates);
465
+ };
466
+ const handleColorChange = color => {
467
+ handleStyleChange({
468
+ color
469
+ });
470
+ };
471
+ const handleFontSizeChange = fontSize => {
472
+ handleStyleChange({
473
+ fontSize
474
+ });
475
+ setShowFontSizePicker(false);
476
+ };
477
+ const handleBorderColorChange = borderColor => {
478
+ handleStyleChange({
479
+ borderColor,
480
+ borderStyle: 'solid'
481
+ });
482
+ };
483
+ const handleBgColorChange = backgroundColor => {
484
+ handleStyleChange({
485
+ backgroundColor
486
+ });
487
+ setShowBgColorPicker(false);
488
+ };
489
+ const handleAllPadding = value => {
490
+ handleStyleChange({
491
+ paddingTop: value,
492
+ paddingRight: value,
493
+ paddingBottom: value,
494
+ paddingLeft: value
495
+ });
496
+ };
497
+ const handleAllMargin = value => {
498
+ handleStyleChange({
499
+ marginTop: value,
500
+ marginRight: value,
501
+ marginBottom: value,
502
+ marginLeft: value
503
+ });
504
+ };
505
+ const parsePx = val => parseInt(val, 10) || 0;
506
+ return /*#__PURE__*/React.createElement("div", {
507
+ className: "toolbar"
508
+ }, /*#__PURE__*/React.createElement("div", {
509
+ className: "toolbar-sections"
510
+ }, /*#__PURE__*/React.createElement("div", {
511
+ className: "toolbar-section"
512
+ }, /*#__PURE__*/React.createElement(ToolbarButton, {
513
+ onClick: onMoveBlockUp,
514
+ disabled: !hasFocusedBlock,
515
+ title: "Move Up"
516
+ }, "\u2191"), /*#__PURE__*/React.createElement(ToolbarButton, {
517
+ onClick: onMoveBlockDown,
518
+ disabled: !hasFocusedBlock,
519
+ title: "Move Down"
520
+ }, "\u2193")), /*#__PURE__*/React.createElement("div", {
521
+ className: "toolbar-divider"
522
+ }), /*#__PURE__*/React.createElement("div", {
523
+ className: "toolbar-section"
524
+ }, /*#__PURE__*/React.createElement("label", {
525
+ className: "toolbar-label"
526
+ }, "Align:"), /*#__PURE__*/React.createElement(ToolbarButton, {
527
+ onClick: () => handleStyleChange({
528
+ textAlign: 'left'
529
+ }),
530
+ disabled: !isTextBlock,
531
+ active: isTextBlock && (styles.textAlign || 'left') === 'left',
532
+ title: "Align Left"
533
+ }, /*#__PURE__*/React.createElement("span", {
534
+ className: "align-icon align-left"
535
+ })), /*#__PURE__*/React.createElement(ToolbarButton, {
536
+ onClick: () => handleStyleChange({
537
+ textAlign: 'center'
538
+ }),
539
+ disabled: !isTextBlock,
540
+ active: isTextBlock && styles.textAlign === 'center',
541
+ title: "Align Center"
542
+ }, /*#__PURE__*/React.createElement("span", {
543
+ className: "align-icon align-center"
544
+ })), /*#__PURE__*/React.createElement(ToolbarButton, {
545
+ onClick: () => handleStyleChange({
546
+ textAlign: 'right'
547
+ }),
548
+ disabled: !isTextBlock,
549
+ active: isTextBlock && styles.textAlign === 'right',
550
+ title: "Align Right"
551
+ }, /*#__PURE__*/React.createElement("span", {
552
+ className: "align-icon align-right"
553
+ }))), /*#__PURE__*/React.createElement("div", {
554
+ className: "toolbar-divider"
555
+ }), /*#__PURE__*/React.createElement("div", {
556
+ className: "toolbar-section"
557
+ }, /*#__PURE__*/React.createElement("label", {
558
+ className: "toolbar-label"
559
+ }, "Font Size:"), /*#__PURE__*/React.createElement("div", {
560
+ className: "toolbar-dropdown"
561
+ }, /*#__PURE__*/React.createElement(ToolbarButton, {
562
+ onClick: () => setShowFontSizePicker(!showFontSizePicker),
563
+ disabled: !isTextBlock,
564
+ title: "Font Size"
565
+ }, styles.fontSize || '16px'), showFontSizePicker && /*#__PURE__*/React.createElement("div", {
566
+ className: "toolbar-dropdown-menu font-size-picker"
567
+ }, FONT_SIZE_OPTIONS.map(size => /*#__PURE__*/React.createElement("button", {
568
+ key: size,
569
+ className: "font-size-option",
570
+ onClick: () => handleFontSizeChange(size)
571
+ }, size))))), /*#__PURE__*/React.createElement("div", {
572
+ className: "toolbar-divider"
573
+ }), /*#__PURE__*/React.createElement("div", {
574
+ className: "toolbar-section"
575
+ }, /*#__PURE__*/React.createElement("label", {
576
+ className: "toolbar-label"
577
+ }, "Text Color:"), /*#__PURE__*/React.createElement("input", {
578
+ type: "color",
579
+ className: "toolbar-color-picker",
580
+ value: styles.color || '#333333',
581
+ onChange: e => handleColorChange(e.target.value),
582
+ disabled: !isTextBlock,
583
+ title: "Text Color"
584
+ })), /*#__PURE__*/React.createElement("div", {
585
+ className: "toolbar-divider"
586
+ }), /*#__PURE__*/React.createElement("div", {
587
+ className: "toolbar-section"
588
+ }, /*#__PURE__*/React.createElement("label", {
589
+ className: "toolbar-label"
590
+ }, "Padding:"), /*#__PURE__*/React.createElement("div", {
591
+ className: "toolbar-dropdown"
592
+ }, /*#__PURE__*/React.createElement(ToolbarButton, {
593
+ onClick: () => setShowPaddingPicker(!showPaddingPicker),
594
+ disabled: !hasFocusedBlock,
595
+ title: "Padding (all sides)"
596
+ }, styles.paddingTop || '0px'), showPaddingPicker && /*#__PURE__*/React.createElement("div", {
597
+ className: "toolbar-dropdown-menu padding-picker"
598
+ }, PADDING_OPTIONS.map(option => /*#__PURE__*/React.createElement("button", {
599
+ key: option,
600
+ className: "padding-option",
601
+ onClick: () => {
602
+ handleAllPadding(option);
603
+ setShowPaddingPicker(false);
604
+ }
605
+ }, option))))), /*#__PURE__*/React.createElement("div", {
606
+ className: "toolbar-divider"
607
+ }), /*#__PURE__*/React.createElement("div", {
608
+ className: "toolbar-section"
609
+ }, /*#__PURE__*/React.createElement("label", {
610
+ className: "toolbar-label"
611
+ }, "Margin:"), /*#__PURE__*/React.createElement("div", {
612
+ className: "toolbar-dropdown"
613
+ }, /*#__PURE__*/React.createElement(ToolbarButton, {
614
+ onClick: () => setShowMarginPicker(!showMarginPicker),
615
+ disabled: !hasFocusedBlock,
616
+ title: "Margin (all sides)"
617
+ }, styles.marginTop || '0px'), showMarginPicker && /*#__PURE__*/React.createElement("div", {
618
+ className: "toolbar-dropdown-menu margin-picker"
619
+ }, MARGIN_OPTIONS.map(option => /*#__PURE__*/React.createElement("button", {
620
+ key: option,
621
+ className: "margin-option",
622
+ onClick: () => {
623
+ handleAllMargin(option);
624
+ setShowMarginPicker(false);
625
+ }
626
+ }, option))))), /*#__PURE__*/React.createElement("div", {
627
+ className: "toolbar-divider"
628
+ }), /*#__PURE__*/React.createElement("div", {
629
+ className: "toolbar-section"
630
+ }, /*#__PURE__*/React.createElement("label", {
631
+ className: "toolbar-label"
632
+ }, "Border Color:"), /*#__PURE__*/React.createElement("input", {
633
+ type: "color",
634
+ className: "toolbar-color-picker",
635
+ value: styles.borderColor || '#cccccc',
636
+ onChange: e => handleBorderColorChange(e.target.value),
637
+ disabled: !hasFocusedBlock,
638
+ title: "Border Color"
639
+ })), /*#__PURE__*/React.createElement("div", {
640
+ className: "toolbar-divider"
641
+ }), /*#__PURE__*/React.createElement("div", {
642
+ className: "toolbar-section"
643
+ }, /*#__PURE__*/React.createElement("label", {
644
+ className: "toolbar-label"
645
+ }, "Border Width:"), /*#__PURE__*/React.createElement("input", {
646
+ type: "number",
647
+ className: "toolbar-input",
648
+ min: "0",
649
+ value: parsePx(styles.borderWidth),
650
+ disabled: !hasFocusedBlock,
651
+ onChange: e => handleStyleChange({
652
+ borderWidth: `${e.target.value}px`,
653
+ borderStyle: 'solid'
654
+ }),
655
+ title: "Border Width"
656
+ })), /*#__PURE__*/React.createElement("div", {
657
+ className: "toolbar-divider"
658
+ }), /*#__PURE__*/React.createElement("div", {
659
+ className: "toolbar-section"
660
+ }, /*#__PURE__*/React.createElement("label", {
661
+ className: "toolbar-label"
662
+ }, "Background Color:"), /*#__PURE__*/React.createElement("div", {
663
+ className: "toolbar-dropdown"
664
+ }, /*#__PURE__*/React.createElement("button", {
665
+ className: "toolbar-color-swatch",
666
+ style: {
667
+ backgroundColor: hasFocusedBlock ? styles.backgroundColor || '#ffffff' : '#cccccc'
668
+ },
669
+ onClick: () => hasFocusedBlock && setShowBgColorPicker(!showBgColorPicker),
670
+ disabled: !hasFocusedBlock,
671
+ title: "Background Color"
672
+ }), showBgColorPicker && /*#__PURE__*/React.createElement("div", {
673
+ className: "toolbar-dropdown-menu color-picker"
674
+ }, COLOR_OPTIONS.map(color => /*#__PURE__*/React.createElement("button", {
675
+ key: color,
676
+ className: "color-option",
677
+ style: {
678
+ backgroundColor: color
679
+ },
680
+ onClick: () => handleBgColorChange(color),
681
+ title: color
682
+ })))))), /*#__PURE__*/React.createElement("div", {
683
+ className: "toolbar-section"
684
+ }, /*#__PURE__*/React.createElement(ToolbarButton, {
685
+ onClick: onSave,
686
+ title: "Save"
687
+ }, "\uD83D\uDCBE Save")));
688
+ }
689
+
690
+ function ParagraphBlock({
691
+ block,
692
+ onChange,
693
+ onFocus,
694
+ onKeyDown,
695
+ isFocused,
696
+ editable = true,
697
+ placeholder
698
+ }) {
699
+ const editorRef = useRef(null);
700
+ const isUpdatingRef = useRef(false);
701
+ useEffect(() => {
702
+ if (editable && isFocused && editorRef.current) {
703
+ editorRef.current.focus();
704
+ }
705
+ }, [isFocused, editable]);
706
+ useEffect(() => {
707
+ // Only update if content changed from outside (not from user typing)
708
+ if (editorRef.current && !isUpdatingRef.current) {
709
+ if (editorRef.current.innerHTML !== block.data.text) {
710
+ editorRef.current.innerHTML = block.data.text || '';
711
+ }
712
+ }
713
+ isUpdatingRef.current = false;
714
+ }, [block.data.text]);
715
+ const handleInput = e => {
716
+ isUpdatingRef.current = true;
717
+ onChange({
718
+ text: e.target.innerHTML
719
+ });
720
+ };
721
+ const handleKeyDown = e => {
722
+ if (onKeyDown) {
723
+ onKeyDown(e);
724
+ }
725
+ };
726
+ if (!editable) {
727
+ return /*#__PURE__*/React.createElement("div", {
728
+ className: "paragraph-block",
729
+ style: block.data.styles,
730
+ dangerouslySetInnerHTML: {
731
+ __html: block.data.text || ''
732
+ }
733
+ });
734
+ }
735
+ return /*#__PURE__*/React.createElement("div", {
736
+ ref: editorRef,
737
+ className: "paragraph-block",
738
+ contentEditable: true,
739
+ suppressContentEditableWarning: true,
740
+ onInput: handleInput,
741
+ onFocus: onFocus,
742
+ onKeyDown: handleKeyDown,
743
+ "data-placeholder": placeholder || "Start typing...",
744
+ style: block.data.styles
745
+ });
746
+ }
747
+
748
+ function HeadingBlock({
749
+ block,
750
+ onChange,
751
+ onFocus,
752
+ onKeyDown,
753
+ isFocused,
754
+ editable = true
755
+ }) {
756
+ const editorRef = useRef(null);
757
+ const isUpdatingRef = useRef(false);
758
+ const HeadingTag = block.data.htmlTag || 'h1';
759
+ useEffect(() => {
760
+ if (editable && isFocused && editorRef.current) {
761
+ editorRef.current.focus();
762
+ }
763
+ }, [isFocused, editable]);
764
+ useEffect(() => {
765
+ if (editorRef.current && !isUpdatingRef.current) {
766
+ if (editorRef.current.innerHTML !== block.data.text) {
767
+ editorRef.current.innerHTML = block.data.text || '';
768
+ }
769
+ }
770
+ isUpdatingRef.current = false;
771
+ }, [block.data.text]);
772
+ const handleInput = e => {
773
+ isUpdatingRef.current = true;
774
+ onChange({
775
+ text: e.target.innerHTML
776
+ });
777
+ };
778
+ const handleKeyDown = e => {
779
+ if (onKeyDown) {
780
+ onKeyDown(e);
781
+ }
782
+ };
783
+ if (!editable) {
784
+ return /*#__PURE__*/React.createElement(HeadingTag, {
785
+ className: "heading-block",
786
+ style: block.data.styles,
787
+ dangerouslySetInnerHTML: {
788
+ __html: block.data.text || ''
789
+ }
790
+ });
791
+ }
792
+ return /*#__PURE__*/React.createElement(HeadingTag, {
793
+ ref: editorRef,
794
+ className: "heading-block",
795
+ contentEditable: true,
796
+ suppressContentEditableWarning: true,
797
+ onInput: handleInput,
798
+ onFocus: onFocus,
799
+ onKeyDown: handleKeyDown,
800
+ "data-placeholder": "Heading",
801
+ style: block.data.styles
802
+ });
803
+ }
804
+
805
+ function ListBlock({
806
+ block,
807
+ onChange,
808
+ onFocus,
809
+ editable = true
810
+ }) {
811
+ const [editingIndex, setEditingIndex] = useState(null);
812
+ // Focus the first item when block is focused/created
813
+ useEffect(() => {
814
+ if (!editable) return;
815
+ if (typeof onFocus === 'function' && editingIndex === null) {
816
+ // Wait for block to be focused by parent
817
+ setTimeout(() => {
818
+ if (listRef.current) {
819
+ const first = listRef.current.querySelector('.list-item-content[data-idx="0"]');
820
+ if (first) {
821
+ first.focus();
822
+ setEditingIndex(0);
823
+ }
824
+ }
825
+ }, 0);
826
+ }
827
+ }, [editable, onFocus, editingIndex]);
828
+ const ListTag = block.data.htmlTag || 'ul';
829
+ const listRef = useRef(null);
830
+ const handleItemChange = (index, value, el) => {
831
+ // Save caret position
832
+ let caretOffset = 0;
833
+ if (el && el.isContentEditable) {
834
+ const selection = window.getSelection();
835
+ if (selection.rangeCount > 0) {
836
+ const range = selection.getRangeAt(0);
837
+ const preCaretRange = range.cloneRange();
838
+ preCaretRange.selectNodeContents(el);
839
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
840
+ caretOffset = preCaretRange.toString().length;
841
+ }
842
+ }
843
+ const newItems = [...block.data.items];
844
+ newItems[index] = value;
845
+ onChange({
846
+ items: newItems
847
+ });
848
+ // Restore caret position
849
+ if (el && el.isContentEditable) {
850
+ setTimeout(() => {
851
+ let node = el;
852
+ let charIndex = 0,
853
+ found = false,
854
+ sel = window.getSelection();
855
+ function setCaret(node) {
856
+ if (found) return;
857
+ if (node.nodeType === 3) {
858
+ // text node
859
+ const nextCharIndex = charIndex + node.length;
860
+ if (!found && caretOffset >= charIndex && caretOffset <= nextCharIndex) {
861
+ const range = document.createRange();
862
+ range.setStart(node, caretOffset - charIndex);
863
+ range.collapse(true);
864
+ sel.removeAllRanges();
865
+ sel.addRange(range);
866
+ found = true;
867
+ }
868
+ charIndex = nextCharIndex;
869
+ } else {
870
+ for (let i = 0; i < node.childNodes.length; i++) {
871
+ setCaret(node.childNodes[i]);
872
+ if (found) break;
873
+ }
874
+ }
875
+ }
876
+ setCaret(node);
877
+ }, 0);
878
+ }
879
+ };
880
+ const handleItemKeyDown = (e, index) => {
881
+ if (e.key === 'Enter') {
882
+ e.preventDefault();
883
+ const newItems = [...block.data.items];
884
+ newItems.splice(index + 1, 0, '');
885
+ onChange({
886
+ items: newItems
887
+ });
888
+ setTimeout(() => {
889
+ setEditingIndex(index + 1);
890
+ // Focus the next item, scoped to this ListBlock only
891
+ if (listRef.current) {
892
+ const next = listRef.current.querySelector(`.list-item-content[data-idx='${index + 1}']`);
893
+ if (next) {
894
+ next.focus();
895
+ }
896
+ }
897
+ }, 0);
898
+ } else if (e.key === 'Backspace' && block.data.items[index] === '') {
899
+ e.preventDefault();
900
+ if (block.data.items.length > 1) {
901
+ const newItems = block.data.items.filter((_, i) => i !== index);
902
+ onChange({
903
+ items: newItems
904
+ });
905
+ setEditingIndex(index > 0 ? index - 1 : 0);
906
+ }
907
+ }
908
+ };
909
+ if (!editable) {
910
+ return /*#__PURE__*/React.createElement(ListTag, {
911
+ className: "list-block",
912
+ style: block.data.styles
913
+ }, block.data.items.map((item, index) => /*#__PURE__*/React.createElement("li", {
914
+ key: index,
915
+ className: "list-item"
916
+ }, /*#__PURE__*/React.createElement("span", {
917
+ dangerouslySetInnerHTML: {
918
+ __html: item
919
+ }
920
+ }))));
921
+ }
922
+ return /*#__PURE__*/React.createElement(ListTag, {
923
+ className: "list-block",
924
+ style: block.data.styles,
925
+ onClick: onFocus,
926
+ ref: listRef
927
+ }, block.data.items.map((item, index) => /*#__PURE__*/React.createElement("li", {
928
+ key: index,
929
+ className: "list-item"
930
+ }, /*#__PURE__*/React.createElement("div", {
931
+ contentEditable: true,
932
+ suppressContentEditableWarning: true,
933
+ "data-idx": index,
934
+ onInput: e => handleItemChange(index, e.target.textContent, e.currentTarget),
935
+ onKeyDown: e => handleItemKeyDown(e, index),
936
+ onFocus: () => setEditingIndex(index),
937
+ dangerouslySetInnerHTML: {
938
+ __html: item
939
+ },
940
+ className: "list-item-content"
941
+ }))));
942
+ }
943
+
944
+ function ImageBlock({
945
+ block,
946
+ onChange,
947
+ onFocus,
948
+ editable = true
949
+ }) {
950
+ const [isEditing, setIsEditing] = useState(!block.data.url);
951
+ const [inputUrl, setInputUrl] = useState(block.data.url || '');
952
+ const handleSubmit = e => {
953
+ e.preventDefault();
954
+ if (inputUrl.trim()) {
955
+ onChange({
956
+ url: inputUrl.trim()
957
+ });
958
+ setIsEditing(false);
959
+ }
960
+ };
961
+ const handleEdit = () => {
962
+ setIsEditing(true);
963
+ setInputUrl(block.data.url || '');
964
+ };
965
+ if (editable && isEditing) {
966
+ return /*#__PURE__*/React.createElement("div", {
967
+ className: "image-block-input",
968
+ style: block.data.styles,
969
+ onClick: onFocus
970
+ }, /*#__PURE__*/React.createElement("form", {
971
+ onSubmit: handleSubmit
972
+ }, /*#__PURE__*/React.createElement("input", {
973
+ type: "text",
974
+ placeholder: "Enter image URL...",
975
+ value: inputUrl,
976
+ onChange: e => setInputUrl(e.target.value),
977
+ autoFocus: true,
978
+ className: "image-url-input"
979
+ }), /*#__PURE__*/React.createElement("div", {
980
+ className: "image-block-actions"
981
+ }, /*#__PURE__*/React.createElement("button", {
982
+ type: "submit",
983
+ className: "image-block-btn"
984
+ }, "Insert"), block.data.url && /*#__PURE__*/React.createElement("button", {
985
+ type: "button",
986
+ onClick: () => setIsEditing(false),
987
+ className: "image-block-btn"
988
+ }, "Cancel"))));
989
+ }
990
+ return /*#__PURE__*/React.createElement("div", {
991
+ className: "image-block-wrapper",
992
+ style: block.data.styles,
993
+ onClick: editable ? onFocus : undefined
994
+ }, /*#__PURE__*/React.createElement("img", {
995
+ src: block.data.url,
996
+ alt: block.data.alt || 'Image',
997
+ className: "image-block",
998
+ onError: e => {
999
+ e.target.style.display = 'none';
1000
+ e.target.nextSibling.style.display = 'block';
1001
+ }
1002
+ }), /*#__PURE__*/React.createElement("div", {
1003
+ className: "image-error",
1004
+ style: {
1005
+ display: 'none'
1006
+ }
1007
+ }, "Failed to load image"), editable && /*#__PURE__*/React.createElement("button", {
1008
+ onClick: handleEdit,
1009
+ className: "image-edit-btn"
1010
+ }, "Edit URL"));
1011
+ }
1012
+
1013
+ function YouTubeBlock({
1014
+ block,
1015
+ onChange,
1016
+ onFocus,
1017
+ editable = true
1018
+ }) {
1019
+ const [isEditing, setIsEditing] = useState(!block.data.videoId);
1020
+ const [inputUrl, setInputUrl] = useState(block.data.url || '');
1021
+ const handleSubmit = e => {
1022
+ e.preventDefault();
1023
+ e.stopPropagation();
1024
+ if (inputUrl.trim()) {
1025
+ const videoId = extractYouTubeId(inputUrl.trim());
1026
+ if (videoId) {
1027
+ onChange({
1028
+ url: inputUrl.trim(),
1029
+ videoId
1030
+ });
1031
+ setIsEditing(false);
1032
+ } else {
1033
+ alert('Invalid YouTube URL. Please enter a valid YouTube video link.');
1034
+ }
1035
+ }
1036
+ };
1037
+ const handleEdit = () => {
1038
+ setIsEditing(true);
1039
+ setInputUrl(block.data.url || '');
1040
+ };
1041
+ if (editable && isEditing) {
1042
+ return /*#__PURE__*/React.createElement("div", {
1043
+ className: "youtube-block-input",
1044
+ style: block.data.styles,
1045
+ onClick: onFocus
1046
+ }, /*#__PURE__*/React.createElement("form", {
1047
+ onSubmit: handleSubmit
1048
+ }, /*#__PURE__*/React.createElement("input", {
1049
+ type: "text",
1050
+ placeholder: "Enter YouTube URL...",
1051
+ value: inputUrl,
1052
+ onChange: e => setInputUrl(e.target.value),
1053
+ autoFocus: true,
1054
+ className: "youtube-url-input"
1055
+ }), /*#__PURE__*/React.createElement("div", {
1056
+ className: "youtube-block-actions"
1057
+ }, /*#__PURE__*/React.createElement("button", {
1058
+ type: "submit",
1059
+ className: "youtube-block-btn"
1060
+ }, "Embed"), block.data.videoId && /*#__PURE__*/React.createElement("button", {
1061
+ type: "button",
1062
+ onClick: () => setIsEditing(false),
1063
+ className: "youtube-block-btn"
1064
+ }, "Cancel"))));
1065
+ }
1066
+ return /*#__PURE__*/React.createElement("div", {
1067
+ className: "youtube-block-wrapper",
1068
+ style: block.data.styles,
1069
+ onClick: editable ? onFocus : undefined
1070
+ }, /*#__PURE__*/React.createElement("div", {
1071
+ className: "youtube-embed"
1072
+ }, /*#__PURE__*/React.createElement("iframe", {
1073
+ src: `https://www.youtube.com/embed/${block.data.videoId}`,
1074
+ frameBorder: "0",
1075
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
1076
+ allowFullScreen: true,
1077
+ title: "YouTube video"
1078
+ })), editable && /*#__PURE__*/React.createElement("button", {
1079
+ onClick: handleEdit,
1080
+ className: "youtube-edit-btn"
1081
+ }, "Change Video"));
1082
+ }
1083
+
1084
+ function DividerBlock({
1085
+ block,
1086
+ onFocus
1087
+ }) {
1088
+ return /*#__PURE__*/React.createElement("div", {
1089
+ className: "divider-block-wrapper",
1090
+ style: block.data.styles,
1091
+ onClick: onFocus
1092
+ }, /*#__PURE__*/React.createElement("hr", {
1093
+ className: "divider-block"
1094
+ }));
1095
+ }
1096
+
1097
+ function ContextMenu({
1098
+ x,
1099
+ y,
1100
+ onAddBlock,
1101
+ onClose
1102
+ }) {
1103
+ const menuRef = useRef(null);
1104
+ useEffect(() => {
1105
+ const handleClickOutside = e => {
1106
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
1107
+ onClose();
1108
+ }
1109
+ };
1110
+ const handleEscape = e => {
1111
+ if (e.key === 'Escape') onClose();
1112
+ };
1113
+ document.addEventListener('mousedown', handleClickOutside);
1114
+ document.addEventListener('keydown', handleEscape);
1115
+ return () => {
1116
+ document.removeEventListener('mousedown', handleClickOutside);
1117
+ document.removeEventListener('keydown', handleEscape);
1118
+ };
1119
+ }, [onClose]);
1120
+ const handleInsert = block => {
1121
+ onAddBlock(block, 'append');
1122
+ onClose();
1123
+ };
1124
+ const items = [{
1125
+ label: '¶ Paragraph',
1126
+ action: () => handleInsert(createEmptyBlock())
1127
+ }, {
1128
+ label: 'H1',
1129
+ action: () => handleInsert(createHeadingBlock(HEADING_LEVELS.H1))
1130
+ }, {
1131
+ label: 'H2',
1132
+ action: () => handleInsert(createHeadingBlock(HEADING_LEVELS.H2))
1133
+ }, {
1134
+ label: 'H3',
1135
+ action: () => handleInsert(createHeadingBlock(HEADING_LEVELS.H3))
1136
+ }, {
1137
+ label: '• Bullet List',
1138
+ action: () => handleInsert(createListBlock(LIST_STYLES.UNORDERED))
1139
+ }, {
1140
+ label: '1. Numbered List',
1141
+ action: () => handleInsert(createListBlock(LIST_STYLES.ORDERED))
1142
+ }, {
1143
+ label: 'Image',
1144
+ action: () => handleInsert(createImageBlock())
1145
+ }, {
1146
+ label: 'YouTube',
1147
+ action: () => handleInsert(createYouTubeBlock())
1148
+ }, {
1149
+ label: '─ Divider',
1150
+ action: () => handleInsert(createDividerBlock())
1151
+ }];
1152
+ return /*#__PURE__*/React.createElement("div", {
1153
+ ref: menuRef,
1154
+ className: "context-menu",
1155
+ style: {
1156
+ left: x,
1157
+ top: y
1158
+ }
1159
+ }, items.map(item => /*#__PURE__*/React.createElement("button", {
1160
+ key: item.label,
1161
+ className: "context-menu-item",
1162
+ onClick: item.action
1163
+ }, item.label)));
1164
+ }
1165
+
1166
+ const TEXT_BLOCK_TYPES = [BLOCK_TYPES.PARAGRAPH, BLOCK_TYPES.HEADING, BLOCK_TYPES.LIST];
1167
+ function StyleField({
1168
+ label,
1169
+ value,
1170
+ onChange,
1171
+ type = 'text',
1172
+ suffix = ''
1173
+ }) {
1174
+ if (type === 'color') {
1175
+ return /*#__PURE__*/React.createElement("div", {
1176
+ className: "style-modal-field"
1177
+ }, /*#__PURE__*/React.createElement("label", null, label), /*#__PURE__*/React.createElement("input", {
1178
+ type: "color",
1179
+ value: value || '#000000',
1180
+ onChange: e => onChange(e.target.value)
1181
+ }));
1182
+ }
1183
+ if (type === 'font-size') {
1184
+ const numericValue = parseInt(value, 10) || 16;
1185
+ return /*#__PURE__*/React.createElement("div", {
1186
+ className: "style-modal-field"
1187
+ }, /*#__PURE__*/React.createElement("label", null, label), /*#__PURE__*/React.createElement("div", {
1188
+ className: "style-modal-font-size"
1189
+ }, /*#__PURE__*/React.createElement("select", {
1190
+ value: FONT_SIZE_OPTIONS.includes(value) ? value : '',
1191
+ onChange: e => {
1192
+ if (e.target.value) onChange(e.target.value);
1193
+ }
1194
+ }, /*#__PURE__*/React.createElement("option", {
1195
+ value: "",
1196
+ disabled: true
1197
+ }, "Common sizes"), FONT_SIZE_OPTIONS.map(size => /*#__PURE__*/React.createElement("option", {
1198
+ key: size,
1199
+ value: size
1200
+ }, size))), /*#__PURE__*/React.createElement("div", {
1201
+ className: "style-modal-input-wrapper"
1202
+ }, /*#__PURE__*/React.createElement("input", {
1203
+ type: "number",
1204
+ min: "1",
1205
+ value: numericValue,
1206
+ onChange: e => onChange(`${e.target.value}px`)
1207
+ }), /*#__PURE__*/React.createElement("span", {
1208
+ className: "style-modal-suffix"
1209
+ }, "px"))));
1210
+ }
1211
+ const numericValue = parseInt(value, 10) || 0;
1212
+ return /*#__PURE__*/React.createElement("div", {
1213
+ className: "style-modal-field"
1214
+ }, /*#__PURE__*/React.createElement("label", null, label), /*#__PURE__*/React.createElement("div", {
1215
+ className: "style-modal-input-wrapper"
1216
+ }, /*#__PURE__*/React.createElement("input", {
1217
+ type: "number",
1218
+ min: "0",
1219
+ value: numericValue,
1220
+ onChange: e => onChange(`${e.target.value}${suffix}`)
1221
+ }), suffix && /*#__PURE__*/React.createElement("span", {
1222
+ className: "style-modal-suffix"
1223
+ }, suffix)));
1224
+ }
1225
+ function StyleModal({
1226
+ block,
1227
+ onUpdateStyle,
1228
+ onClose
1229
+ }) {
1230
+ const styles = block.data.styles || {};
1231
+ const isTextBlock = TEXT_BLOCK_TYPES.includes(block.type);
1232
+ const handleChange = (prop, value) => {
1233
+ const updates = {
1234
+ [prop]: value
1235
+ };
1236
+ if (prop === 'borderWidth' || prop === 'borderColor') {
1237
+ updates.borderStyle = 'solid';
1238
+ }
1239
+ onUpdateStyle(block.id, updates);
1240
+ };
1241
+ const handleAllMargins = value => {
1242
+ onUpdateStyle(block.id, {
1243
+ marginTop: value,
1244
+ marginRight: value,
1245
+ marginBottom: value,
1246
+ marginLeft: value
1247
+ });
1248
+ };
1249
+ const handleAllPadding = value => {
1250
+ onUpdateStyle(block.id, {
1251
+ paddingTop: value,
1252
+ paddingRight: value,
1253
+ paddingBottom: value,
1254
+ paddingLeft: value
1255
+ });
1256
+ };
1257
+ return /*#__PURE__*/React.createElement("div", {
1258
+ className: "style-modal-overlay",
1259
+ onMouseDown: onClose
1260
+ }, /*#__PURE__*/React.createElement("div", {
1261
+ className: "style-modal",
1262
+ onMouseDown: e => e.stopPropagation()
1263
+ }, /*#__PURE__*/React.createElement("div", {
1264
+ className: "style-modal-header"
1265
+ }, /*#__PURE__*/React.createElement("h3", null, "Edit Block Style"), /*#__PURE__*/React.createElement("button", {
1266
+ className: "style-modal-close",
1267
+ onClick: onClose
1268
+ }, "\xD7")), /*#__PURE__*/React.createElement("div", {
1269
+ className: "style-modal-body"
1270
+ }, isTextBlock && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
1271
+ className: "style-modal-section"
1272
+ }, /*#__PURE__*/React.createElement("h4", null, "Text"), /*#__PURE__*/React.createElement("div", {
1273
+ className: "style-modal-row"
1274
+ }, /*#__PURE__*/React.createElement(StyleField, {
1275
+ label: "Font Size",
1276
+ value: styles.fontSize,
1277
+ onChange: v => handleChange('fontSize', v),
1278
+ type: "font-size"
1279
+ }), /*#__PURE__*/React.createElement(StyleField, {
1280
+ label: "Color",
1281
+ value: styles.color,
1282
+ onChange: v => handleChange('color', v),
1283
+ type: "color"
1284
+ }))), /*#__PURE__*/React.createElement("hr", {
1285
+ className: "style-modal-divider"
1286
+ })), /*#__PURE__*/React.createElement("div", {
1287
+ className: "style-modal-section"
1288
+ }, /*#__PURE__*/React.createElement("h4", null, "Margin"), /*#__PURE__*/React.createElement("div", {
1289
+ className: "style-modal-row"
1290
+ }, /*#__PURE__*/React.createElement(StyleField, {
1291
+ label: "All",
1292
+ value: styles.marginAll || '',
1293
+ onChange: v => {
1294
+ handleAllMargins(v);
1295
+ handleChange('marginAll', v);
1296
+ },
1297
+ suffix: "px"
1298
+ }), /*#__PURE__*/React.createElement(StyleField, {
1299
+ label: "Top",
1300
+ value: styles.marginTop,
1301
+ onChange: v => handleChange('marginTop', v),
1302
+ suffix: "px"
1303
+ }), /*#__PURE__*/React.createElement(StyleField, {
1304
+ label: "Right",
1305
+ value: styles.marginRight,
1306
+ onChange: v => handleChange('marginRight', v),
1307
+ suffix: "px"
1308
+ }), /*#__PURE__*/React.createElement(StyleField, {
1309
+ label: "Bottom",
1310
+ value: styles.marginBottom,
1311
+ onChange: v => handleChange('marginBottom', v),
1312
+ suffix: "px"
1313
+ }), /*#__PURE__*/React.createElement(StyleField, {
1314
+ label: "Left",
1315
+ value: styles.marginLeft,
1316
+ onChange: v => handleChange('marginLeft', v),
1317
+ suffix: "px"
1318
+ }))), /*#__PURE__*/React.createElement("hr", {
1319
+ className: "style-modal-divider"
1320
+ }), /*#__PURE__*/React.createElement("div", {
1321
+ className: "style-modal-section"
1322
+ }, /*#__PURE__*/React.createElement("h4", null, "Padding"), /*#__PURE__*/React.createElement("div", {
1323
+ className: "style-modal-row"
1324
+ }, /*#__PURE__*/React.createElement(StyleField, {
1325
+ label: "All",
1326
+ value: styles.paddingAll || '',
1327
+ onChange: v => {
1328
+ handleAllPadding(v);
1329
+ handleChange('paddingAll', v);
1330
+ },
1331
+ suffix: "px"
1332
+ }), /*#__PURE__*/React.createElement(StyleField, {
1333
+ label: "Top",
1334
+ value: styles.paddingTop,
1335
+ onChange: v => handleChange('paddingTop', v),
1336
+ suffix: "px"
1337
+ }), /*#__PURE__*/React.createElement(StyleField, {
1338
+ label: "Right",
1339
+ value: styles.paddingRight,
1340
+ onChange: v => handleChange('paddingRight', v),
1341
+ suffix: "px"
1342
+ }), /*#__PURE__*/React.createElement(StyleField, {
1343
+ label: "Bottom",
1344
+ value: styles.paddingBottom,
1345
+ onChange: v => handleChange('paddingBottom', v),
1346
+ suffix: "px"
1347
+ }), /*#__PURE__*/React.createElement(StyleField, {
1348
+ label: "Left",
1349
+ value: styles.paddingLeft,
1350
+ onChange: v => handleChange('paddingLeft', v),
1351
+ suffix: "px"
1352
+ }))), /*#__PURE__*/React.createElement("hr", {
1353
+ className: "style-modal-divider"
1354
+ }), /*#__PURE__*/React.createElement("div", {
1355
+ className: "style-modal-section"
1356
+ }, /*#__PURE__*/React.createElement("h4", null, "Border"), /*#__PURE__*/React.createElement("div", {
1357
+ className: "style-modal-row"
1358
+ }, /*#__PURE__*/React.createElement(StyleField, {
1359
+ label: "Width",
1360
+ value: styles.borderWidth,
1361
+ onChange: v => handleChange('borderWidth', v),
1362
+ suffix: "px"
1363
+ }), /*#__PURE__*/React.createElement(StyleField, {
1364
+ label: "Color",
1365
+ value: styles.borderColor,
1366
+ onChange: v => handleChange('borderColor', v),
1367
+ type: "color"
1368
+ }))), /*#__PURE__*/React.createElement("hr", {
1369
+ className: "style-modal-divider"
1370
+ }), /*#__PURE__*/React.createElement("div", {
1371
+ className: "style-modal-section"
1372
+ }, /*#__PURE__*/React.createElement("h4", null, "Background"), /*#__PURE__*/React.createElement("div", {
1373
+ className: "style-modal-row"
1374
+ }, /*#__PURE__*/React.createElement(StyleField, {
1375
+ label: "Color",
1376
+ value: styles.backgroundColor,
1377
+ onChange: v => handleChange('backgroundColor', v),
1378
+ type: "color"
1379
+ }))))));
1380
+ }
1381
+
1382
+ function Editor({
1383
+ initialData,
1384
+ onSave,
1385
+ placeholder = 'Start writing...',
1386
+ editable = true,
1387
+ showBranding = true
1388
+ }) {
1389
+ const editorContentRef = useRef(null);
1390
+ const blockRefs = useRef({});
1391
+ const [contextMenu, setContextMenu] = useState({
1392
+ visible: false,
1393
+ x: 0,
1394
+ y: 0
1395
+ });
1396
+ const [editingBlockId, setEditingBlockId] = useState(null);
1397
+ const [showSaveNotification, setShowSaveNotification] = useState(false);
1398
+ const {
1399
+ blocks,
1400
+ focusedBlockId,
1401
+ setFocusedBlockId,
1402
+ addBlock,
1403
+ updateBlock,
1404
+ updateBlockStyle,
1405
+ deleteBlock,
1406
+ moveBlock,
1407
+ getJSON
1408
+ } = useEditor(initialData);
1409
+
1410
+ // Global ESC key handler to deselect
1411
+ useEffect(() => {
1412
+ if (!editable) return;
1413
+ const handleGlobalKeyDown = e => {
1414
+ if (e.key === 'Escape' && focusedBlockId) {
1415
+ setFocusedBlockId(null);
1416
+ if (document.activeElement) {
1417
+ document.activeElement.blur();
1418
+ }
1419
+ }
1420
+ };
1421
+ document.addEventListener('keydown', handleGlobalKeyDown);
1422
+ return () => document.removeEventListener('keydown', handleGlobalKeyDown);
1423
+ }, [focusedBlockId, editable, setFocusedBlockId]);
1424
+ const handleSave = () => {
1425
+ const json = getJSON();
1426
+ if (onSave) {
1427
+ onSave(json);
1428
+ }
1429
+ setShowSaveNotification(true);
1430
+ setTimeout(() => setShowSaveNotification(false), 1800);
1431
+ };
1432
+ const handleBlockChange = (blockId, updates) => {
1433
+ updateBlock(blockId, updates);
1434
+ };
1435
+ const handleBlockStyleChange = styleUpdates => {
1436
+ if (focusedBlockId) {
1437
+ updateBlockStyle(focusedBlockId, styleUpdates);
1438
+ }
1439
+ };
1440
+ const handleMoveBlockUp = () => {
1441
+ if (!focusedBlockId) return;
1442
+ const index = blocks.findIndex(b => b.id === focusedBlockId);
1443
+ if (index > 0) {
1444
+ moveBlock(index, index - 1);
1445
+ }
1446
+ };
1447
+ const handleMoveBlockDown = () => {
1448
+ if (!focusedBlockId) return;
1449
+ const index = blocks.findIndex(b => b.id === focusedBlockId);
1450
+ if (index < blocks.length - 1) {
1451
+ moveBlock(index, index + 1);
1452
+ }
1453
+ };
1454
+ const handleAddBlock = (block, mode = 'append') => {
1455
+ if (mode === 'replace' && focusedBlockId) {
1456
+ const focusedIndex = blocks.findIndex(b => b.id === focusedBlockId);
1457
+ if (focusedIndex !== -1) {
1458
+ deleteBlock(focusedBlockId);
1459
+ addBlock(block, focusedIndex);
1460
+ setTimeout(() => setFocusedBlockId(block.id), 0);
1461
+ return;
1462
+ }
1463
+ }
1464
+ addBlock(block, blocks.length);
1465
+ setTimeout(() => setFocusedBlockId(block.id), 0);
1466
+ };
1467
+ const handleContextMenu = e => {
1468
+ if (!editable) return;
1469
+ e.preventDefault();
1470
+ const rect = editorContentRef.current.getBoundingClientRect();
1471
+ setContextMenu({
1472
+ visible: true,
1473
+ x: e.clientX - rect.left,
1474
+ y: e.clientY - rect.top
1475
+ });
1476
+ };
1477
+ const handleMouseDown = e => {
1478
+ if (!editable) return;
1479
+ let clickedBlockId = null;
1480
+ for (let blockId in blockRefs.current) {
1481
+ const blockEl = blockRefs.current[blockId];
1482
+ if (blockEl && blockEl.contains(e.target)) {
1483
+ clickedBlockId = blockId;
1484
+ break;
1485
+ }
1486
+ }
1487
+ if (clickedBlockId) {
1488
+ setFocusedBlockId(clickedBlockId);
1489
+ } else {
1490
+ setFocusedBlockId(null);
1491
+ }
1492
+ };
1493
+ const handleKeyDown = (blockId, e) => {
1494
+ if (e.key === 'Escape') {
1495
+ e.preventDefault();
1496
+ setFocusedBlockId(null);
1497
+ if (document.activeElement) {
1498
+ document.activeElement.blur();
1499
+ }
1500
+ return;
1501
+ }
1502
+ const blockIndex = blocks.findIndex(b => b.id === blockId);
1503
+ const block = blocks[blockIndex];
1504
+ if (e.key === 'Backspace' && block.data.text === '') {
1505
+ e.preventDefault();
1506
+ deleteBlock(blockId);
1507
+ if (blockIndex > 0) {
1508
+ setFocusedBlockId(blocks[blockIndex - 1].id);
1509
+ }
1510
+ }
1511
+ };
1512
+ const renderBlock = block => {
1513
+ const isFocused = editable && block.id === focusedBlockId;
1514
+ const commonProps = {
1515
+ block,
1516
+ onChange: updates => handleBlockChange(block.id, updates),
1517
+ onFocus: () => {},
1518
+ isFocused,
1519
+ editable,
1520
+ placeholder
1521
+ };
1522
+ switch (block.type) {
1523
+ case BLOCK_TYPES.PARAGRAPH:
1524
+ return /*#__PURE__*/React.createElement(ParagraphBlock, _extends({}, commonProps, {
1525
+ onKeyDown: editable ? e => handleKeyDown(block.id, e) : undefined
1526
+ }));
1527
+ case BLOCK_TYPES.HEADING:
1528
+ return /*#__PURE__*/React.createElement(HeadingBlock, _extends({}, commonProps, {
1529
+ onKeyDown: editable ? e => handleKeyDown(block.id, e) : undefined
1530
+ }));
1531
+ case BLOCK_TYPES.LIST:
1532
+ return /*#__PURE__*/React.createElement(ListBlock, commonProps);
1533
+ case BLOCK_TYPES.IMAGE:
1534
+ return /*#__PURE__*/React.createElement(ImageBlock, commonProps);
1535
+ case BLOCK_TYPES.YOUTUBE:
1536
+ return /*#__PURE__*/React.createElement(YouTubeBlock, commonProps);
1537
+ case BLOCK_TYPES.DIVIDER:
1538
+ return /*#__PURE__*/React.createElement(DividerBlock, commonProps);
1539
+ default:
1540
+ return null;
1541
+ }
1542
+ };
1543
+ const getBlockTagLabel = block => {
1544
+ switch (block.type) {
1545
+ case BLOCK_TYPES.PARAGRAPH:
1546
+ return 'p';
1547
+ case BLOCK_TYPES.HEADING:
1548
+ return block.data.htmlTag || 'h1';
1549
+ case BLOCK_TYPES.LIST:
1550
+ return block.data.style === LIST_STYLES.ORDERED ? 'ol' : 'ul';
1551
+ case BLOCK_TYPES.IMAGE:
1552
+ return 'img';
1553
+ case BLOCK_TYPES.YOUTUBE:
1554
+ return 'iframe';
1555
+ case BLOCK_TYPES.DIVIDER:
1556
+ return 'hr';
1557
+ default:
1558
+ return 'div';
1559
+ }
1560
+ };
1561
+ const focusedBlock = blocks.find(b => b.id === focusedBlockId);
1562
+ return /*#__PURE__*/React.createElement("div", {
1563
+ className: `evolution-editor ${!editable ? 'read-only' : ''}`
1564
+ }, editable && /*#__PURE__*/React.createElement(React.Fragment, null, showBranding && /*#__PURE__*/React.createElement("div", {
1565
+ className: "editor-branding"
1566
+ }, /*#__PURE__*/React.createElement("div", {
1567
+ className: "editor-logo"
1568
+ }, "Evolution Editor"), /*#__PURE__*/React.createElement("div", {
1569
+ className: "editor-subtitle"
1570
+ }, "Developed by ", /*#__PURE__*/React.createElement("a", {
1571
+ href: "https://github.com/james-evolution",
1572
+ target: "_blank",
1573
+ rel: "noopener noreferrer"
1574
+ }, "@james-evolution")), showSaveNotification && /*#__PURE__*/React.createElement("div", {
1575
+ className: "editor-save-notification"
1576
+ }, "JSON exported successfully")), !showBranding && showSaveNotification && /*#__PURE__*/React.createElement("div", {
1577
+ className: "editor-save-notification editor-save-notification-no-brand"
1578
+ }, "JSON exported successfully"), /*#__PURE__*/React.createElement("div", {
1579
+ className: "editor-stepper"
1580
+ }, /*#__PURE__*/React.createElement("div", {
1581
+ className: "step"
1582
+ }, /*#__PURE__*/React.createElement("span", {
1583
+ className: "step-label"
1584
+ }, "Add Element"), /*#__PURE__*/React.createElement("span", {
1585
+ className: "step-desc"
1586
+ }, "Right-click anywhere to open the menu for inserting elements.")), /*#__PURE__*/React.createElement("div", {
1587
+ className: "step"
1588
+ }, /*#__PURE__*/React.createElement("span", {
1589
+ className: "step-label"
1590
+ }, "Style Element"), /*#__PURE__*/React.createElement("span", {
1591
+ className: "step-desc"
1592
+ }, "Click the element to unlock toolbar styling options, or hover over the element and click \u270F\uFE0F to style.")), /*#__PURE__*/React.createElement("div", {
1593
+ className: "step"
1594
+ }, /*#__PURE__*/React.createElement("span", {
1595
+ className: "step-label"
1596
+ }, "Reorder Element"), /*#__PURE__*/React.createElement("span", {
1597
+ className: "step-desc"
1598
+ }, "Use \u2191 \u2193 on the toolbar or hover an element and click \u2195\uFE0F to reorder.")), /*#__PURE__*/React.createElement("div", {
1599
+ className: "step"
1600
+ }, /*#__PURE__*/React.createElement("span", {
1601
+ className: "step-label"
1602
+ }, "Delete Element"), /*#__PURE__*/React.createElement("span", {
1603
+ className: "step-desc"
1604
+ }, "Hover over an element and click \uD83D\uDDD1\uFE0F to delete it."))), /*#__PURE__*/React.createElement(Toolbar, {
1605
+ focusedBlock: focusedBlock,
1606
+ onUpdateBlockStyle: handleBlockStyleChange,
1607
+ onMoveBlockUp: handleMoveBlockUp,
1608
+ onMoveBlockDown: handleMoveBlockDown,
1609
+ onSave: handleSave
1610
+ })), /*#__PURE__*/React.createElement("div", {
1611
+ ref: editorContentRef,
1612
+ className: "editor-content",
1613
+ onMouseDown: handleMouseDown,
1614
+ onContextMenu: editable ? handleContextMenu : undefined
1615
+ }, blocks.map((block, idx) => /*#__PURE__*/React.createElement("div", {
1616
+ key: block.id,
1617
+ className: "editor-block-wrapper"
1618
+ }, /*#__PURE__*/React.createElement("span", {
1619
+ className: "block-tag-label"
1620
+ }, "<", getBlockTagLabel(block), ">"), /*#__PURE__*/React.createElement("div", {
1621
+ className: "block-left-controls"
1622
+ }, editable && /*#__PURE__*/React.createElement("div", {
1623
+ className: "block-move-controls"
1624
+ }, /*#__PURE__*/React.createElement("button", {
1625
+ className: "block-move-btn",
1626
+ onClick: () => idx > 0 && moveBlock(idx, idx - 1),
1627
+ title: "Move up",
1628
+ disabled: idx === 0
1629
+ }, "\u2191"), /*#__PURE__*/React.createElement("button", {
1630
+ className: "block-move-btn",
1631
+ onClick: () => idx < blocks.length - 1 && moveBlock(idx, idx + 1),
1632
+ title: "Move down",
1633
+ disabled: idx === blocks.length - 1
1634
+ }, "\u2193"))), /*#__PURE__*/React.createElement("div", {
1635
+ ref: editable ? el => blockRefs.current[block.id] = el : undefined,
1636
+ className: `editor-block ${editable && block.id === focusedBlockId ? 'focused' : ''}`,
1637
+ "data-block-id": block.id
1638
+ }, renderBlock(block)), editable && /*#__PURE__*/React.createElement("div", {
1639
+ className: "block-actions"
1640
+ }, /*#__PURE__*/React.createElement("button", {
1641
+ className: "block-edit-button",
1642
+ onClick: () => setEditingBlockId(block.id),
1643
+ title: "Edit block style"
1644
+ }, "\u270F\uFE0F"), /*#__PURE__*/React.createElement("button", {
1645
+ className: "block-delete-button",
1646
+ onClick: () => deleteBlock(block.id),
1647
+ title: "Delete block"
1648
+ }, "\uD83D\uDDD1\uFE0F")))), editable && contextMenu.visible && /*#__PURE__*/React.createElement(ContextMenu, {
1649
+ x: contextMenu.x,
1650
+ y: contextMenu.y,
1651
+ onAddBlock: handleAddBlock,
1652
+ onClose: () => setContextMenu({
1653
+ visible: false,
1654
+ x: 0,
1655
+ y: 0
1656
+ })
1657
+ })), editable && editingBlockId && /*#__PURE__*/React.createElement(StyleModal, {
1658
+ block: blocks.find(b => b.id === editingBlockId),
1659
+ onUpdateStyle: (blockId, styleUpdates) => updateBlockStyle(blockId, styleUpdates),
1660
+ onClose: () => setEditingBlockId(null)
1661
+ }));
1662
+ }
1663
+
1664
+ export { BLOCK_TYPES, Editor as EvolutionEditor, HEADING_LEVELS, LIST_STYLES, deserialize, serialize };
1665
+ //# sourceMappingURL=index.esm.js.map