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