@reiwuzen/blocky-react 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,667 @@
1
+ // src/components/editor.tsx
2
+ import { createContext, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo2, useRef as useRef3 } from "react";
3
+ import { createBlock } from "@reiwuzen/blocky";
4
+
5
+ // src/store/editor-store.ts
6
+ import { create } from "zustand";
7
+ import {
8
+ insertBlockAfter,
9
+ deleteBlock,
10
+ duplicateBlock,
11
+ moveBlock
12
+ } from "@reiwuzen/blocky";
13
+ function createEditorStore(config = {}) {
14
+ return create((set, get) => ({
15
+ blocks: config.initialBlocks ?? [],
16
+ activeBlockId: null,
17
+ setBlocks: (blocks) => {
18
+ set({ blocks });
19
+ config.onChange?.(blocks);
20
+ },
21
+ updateBlock: (block) => {
22
+ const blocks = get().blocks.map((b) => b.id === block.id ? block : b);
23
+ set({ blocks });
24
+ config.onChange?.(blocks);
25
+ },
26
+ insertAfter: (afterId, type = "paragraph") => {
27
+ const result = insertBlockAfter(get().blocks, afterId, type);
28
+ if (!result.ok) return null;
29
+ const { blocks, newId } = result.value;
30
+ set({ blocks, activeBlockId: newId });
31
+ config.onChange?.(blocks);
32
+ return newId;
33
+ },
34
+ removeBlock: (id) => {
35
+ const { blocks, prevId } = deleteBlock(get().blocks, id);
36
+ set({ blocks, activeBlockId: prevId });
37
+ config.onChange?.(blocks);
38
+ return prevId;
39
+ },
40
+ duplicate: (id) => {
41
+ const block = get().blocks.find((b) => b.id === id);
42
+ if (!block) return;
43
+ const dup = duplicateBlock(block, crypto.randomUUID());
44
+ const result = insertBlockAfter(get().blocks, id, block.type);
45
+ if (!result.ok) return;
46
+ const blocks = result.value.blocks.map(
47
+ (b) => b.id === result.value.newId ? dup : b
48
+ );
49
+ set({ blocks, activeBlockId: dup.id });
50
+ config.onChange?.(blocks);
51
+ },
52
+ move: (id, direction) => {
53
+ moveBlock(get().blocks, id, direction).match(
54
+ (blocks) => {
55
+ set({ blocks });
56
+ config.onChange?.(blocks);
57
+ },
58
+ () => {
59
+ }
60
+ );
61
+ },
62
+ setActiveBlockId: (id) => set({ activeBlockId: id })
63
+ }));
64
+ }
65
+
66
+ // src/components/blockList.tsx
67
+ import { useCallback as useCallback2 } from "react";
68
+
69
+ // src/hooks/useEditor.ts
70
+ import { useContext, useMemo } from "react";
71
+ import { useStore } from "zustand";
72
+ function useEditor(selector) {
73
+ const store = useContext(EditorContext);
74
+ if (!store) throw new Error("useEditor must be used inside <Editor />");
75
+ return useStore(store, selector);
76
+ }
77
+ function useBlocks() {
78
+ return useEditor((s) => s.blocks);
79
+ }
80
+ function useActiveBlockId() {
81
+ return useEditor((s) => s.activeBlockId);
82
+ }
83
+ function useEditorActions() {
84
+ const setBlocks = useEditor((s) => s.setBlocks);
85
+ const updateBlock = useEditor((s) => s.updateBlock);
86
+ const insertBlockAfter2 = useEditor((s) => s.insertAfter);
87
+ const removeBlock = useEditor((s) => s.removeBlock);
88
+ const duplicateBlock2 = useEditor((s) => s.duplicate);
89
+ const moveBlock2 = useEditor((s) => s.move);
90
+ const setActiveBlockId = useEditor((s) => s.setActiveBlockId);
91
+ return useMemo(() => ({
92
+ setBlocks,
93
+ updateBlock,
94
+ insertBlockAfter: insertBlockAfter2,
95
+ removeBlock,
96
+ duplicateBlock: duplicateBlock2,
97
+ moveBlock: moveBlock2,
98
+ setActiveBlockId
99
+ }), [
100
+ setBlocks,
101
+ updateBlock,
102
+ insertBlockAfter2,
103
+ removeBlock,
104
+ duplicateBlock2,
105
+ moveBlock2,
106
+ setActiveBlockId
107
+ ]);
108
+ }
109
+
110
+ // src/components/block.tsx
111
+ import { useState as useState2 } from "react";
112
+
113
+ // src/hooks/useBlockKeyboard.ts
114
+ import { useCallback } from "react";
115
+ import {
116
+ splitBlock,
117
+ mergeBlocks,
118
+ flatToPosition,
119
+ applyMarkdownTransform,
120
+ indentBlock,
121
+ outdentBlock
122
+ } from "@reiwuzen/blocky";
123
+ function useBlockKeyboard({ block, onFocus }) {
124
+ const { insertBlockAfter: insertBlockAfter2, removeBlock, updateBlock } = useEditorActions();
125
+ const blocks = useBlocks();
126
+ return useCallback((e) => {
127
+ const fresh = blocks.find((b) => b.id === block.id) ?? block;
128
+ const sel = window.getSelection();
129
+ const flat = sel?.anchorOffset ?? 0;
130
+ if (e.key === "Enter" && !e.shiftKey) {
131
+ e.preventDefault();
132
+ flatToPosition(fresh, flat).match(
133
+ ({ nodeIndex, offset }) => {
134
+ splitBlock(fresh, nodeIndex, offset).match(
135
+ ([original, newBlock]) => {
136
+ updateBlock(original);
137
+ const newId = insertBlockAfter2(fresh.id, "paragraph");
138
+ if (newId) {
139
+ updateBlock({ ...newBlock, id: newId });
140
+ onFocus(newId);
141
+ setTimeout(() => {
142
+ document.querySelector(`[data-block-id="${newId}"]`)?.focus();
143
+ }, 0);
144
+ }
145
+ },
146
+ () => {
147
+ }
148
+ );
149
+ },
150
+ () => {
151
+ }
152
+ );
153
+ return;
154
+ }
155
+ if (e.key === "Backspace" && flat === 0) {
156
+ const index = blocks.findIndex((b) => b.id === fresh.id);
157
+ if (index === 0) return;
158
+ const prev = blocks[index - 1];
159
+ if (prev.type === "code" || prev.type === "equation") return;
160
+ e.preventDefault();
161
+ mergeBlocks(prev, fresh).match(
162
+ (merged) => {
163
+ updateBlock(merged);
164
+ removeBlock(fresh.id);
165
+ onFocus(merged.id);
166
+ setTimeout(() => {
167
+ document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
168
+ }, 0);
169
+ },
170
+ () => {
171
+ }
172
+ );
173
+ return;
174
+ }
175
+ if (e.key === " ") {
176
+ applyMarkdownTransform(fresh, flat).match(
177
+ ({ block: transformed, converted }) => {
178
+ if (converted) {
179
+ e.preventDefault();
180
+ updateBlock(transformed);
181
+ }
182
+ },
183
+ () => {
184
+ }
185
+ );
186
+ return;
187
+ }
188
+ if (e.key === "Tab") {
189
+ e.preventDefault();
190
+ const fn = e.shiftKey ? outdentBlock : indentBlock;
191
+ fn(fresh).match(
192
+ (b) => updateBlock(b),
193
+ () => {
194
+ }
195
+ );
196
+ }
197
+ }, [block.id, blocks, insertBlockAfter2, removeBlock, updateBlock, onFocus]);
198
+ }
199
+
200
+ // src/components/blocks/editableContent.tsx
201
+ import { jsx } from "react/jsx-runtime";
202
+ function EditableContent({
203
+ block,
204
+ className,
205
+ placeholder,
206
+ editable,
207
+ onFocus,
208
+ blockRefs,
209
+ hydratedBlocks
210
+ }) {
211
+ const { updateBlock } = useEditorActions();
212
+ const blocks = useBlocks();
213
+ const handleKeyDown = useBlockKeyboard({ block, onFocus });
214
+ const initialText = getInitialText(block.content);
215
+ const handleInput = (e) => {
216
+ const el = e.currentTarget;
217
+ const text = el.textContent ?? "";
218
+ updateBlock({ ...block, content: [{ type: "text", text }] });
219
+ };
220
+ return /* @__PURE__ */ jsx(
221
+ "span",
222
+ {
223
+ "data-block-id": block.id,
224
+ className: `blocky-block-content ${className ?? ""}`,
225
+ "data-placeholder": placeholder,
226
+ contentEditable: editable,
227
+ suppressContentEditableWarning: true,
228
+ ref: (el) => {
229
+ if (!el) return;
230
+ blockRefs.current.set(block.id, el);
231
+ if (hydratedBlocks.current.has(block.id)) return;
232
+ el.innerHTML = nodesToHtml(block.content);
233
+ hydratedBlocks.current.add(block.id);
234
+ },
235
+ onInput: handleInput,
236
+ onKeyDown: handleKeyDown,
237
+ onFocus: () => onFocus(block.id)
238
+ }
239
+ );
240
+ }
241
+ function getInitialText(nodes) {
242
+ return nodes.map((n) => {
243
+ if (n.type === "text") return n.text;
244
+ if (n.type === "code") return n.text;
245
+ if (n.type === "equation") return n.latex;
246
+ return "";
247
+ }).join("");
248
+ }
249
+ function nodesToHtml(nodes) {
250
+ return nodes.map((n) => {
251
+ if (n.type === "code") return `<code><span>${esc(n.text)}</span></code>`;
252
+ if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
253
+ let inner = `<span>${esc(n.text)}</span>`;
254
+ if (n.bold) inner = `<strong>${inner}</strong>`;
255
+ if (n.italic) inner = `<em>${inner}</em>`;
256
+ if (n.underline) inner = `<u>${inner}</u>`;
257
+ if (n.strikethrough) inner = `<s>${inner}</s>`;
258
+ if (n.highlighted) inner = `<mark class="blocky-highlight-${n.highlighted}">${inner}</mark>`;
259
+ if (n.color) inner = `<span class="blocky-color-${n.color}">${inner}</span>`;
260
+ if (n.link) inner = `<a href="${n.link}">${inner}</a>`;
261
+ return inner;
262
+ }).join("");
263
+ }
264
+ function domToNodes(el) {
265
+ const nodes = [];
266
+ const walk = (node, formats = {}) => {
267
+ if (node.nodeType === window.Node.TEXT_NODE) {
268
+ const text = node.textContent ?? "";
269
+ if (!text) return;
270
+ nodes.push({ type: "text", text, ...formats });
271
+ return;
272
+ }
273
+ if (!(node instanceof HTMLElement)) return;
274
+ const tag = node.tagName.toLowerCase();
275
+ const inherited = { ...formats };
276
+ if (tag === "strong" || tag === "b") inherited.bold = true;
277
+ if (tag === "em" || tag === "i") inherited.italic = true;
278
+ if (tag === "u") inherited.underline = true;
279
+ if (tag === "s") inherited.strikethrough = true;
280
+ if (tag === "a") inherited.link = node.getAttribute("href") ?? void 0;
281
+ if (tag === "mark") {
282
+ inherited.highlighted = node.className.includes("green") ? "green" : "yellow";
283
+ }
284
+ if (tag === "code") {
285
+ nodes.push({ type: "code", text: node.innerText });
286
+ return;
287
+ }
288
+ if (tag === "span" && node.classList.contains("blocky-equation")) {
289
+ nodes.push({ type: "equation", latex: node.innerText });
290
+ return;
291
+ }
292
+ node.childNodes.forEach((child) => walk(child, inherited));
293
+ };
294
+ el.childNodes.forEach((child) => walk(child));
295
+ const merged = [];
296
+ for (const node of nodes) {
297
+ const prev = merged[merged.length - 1];
298
+ if (prev && node.type === "text" && prev.type === "text" && JSON.stringify({ ...prev, text: "" }) === JSON.stringify({ ...node, text: "" })) {
299
+ prev.text += node.text;
300
+ } else {
301
+ merged.push({ ...node });
302
+ }
303
+ }
304
+ return merged.length > 0 ? merged : [{ type: "text", text: "" }];
305
+ }
306
+ function esc(s) {
307
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
308
+ }
309
+
310
+ // src/components/drag/DragHandle.tsx
311
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
312
+ function DragHandle({ blockId, onDragStart }) {
313
+ return /* @__PURE__ */ jsx2(
314
+ "div",
315
+ {
316
+ className: "blocky-drag-handle",
317
+ draggable: true,
318
+ onDragStart: (e) => {
319
+ e.dataTransfer.effectAllowed = "move";
320
+ e.dataTransfer.setData("text/plain", blockId);
321
+ onDragStart(blockId);
322
+ },
323
+ title: "Drag to reorder",
324
+ children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "16", viewBox: "0 0 10 16", fill: "currentColor", children: [
325
+ /* @__PURE__ */ jsx2("circle", { cx: "2", cy: "2", r: "1.5" }),
326
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "2", r: "1.5" }),
327
+ /* @__PURE__ */ jsx2("circle", { cx: "2", cy: "8", r: "1.5" }),
328
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "8", r: "1.5" }),
329
+ /* @__PURE__ */ jsx2("circle", { cx: "2", cy: "14", r: "1.5" }),
330
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "14", r: "1.5" })
331
+ ] })
332
+ }
333
+ );
334
+ }
335
+
336
+ // src/components/toolbar/BlockTypeSwitcher.tsx
337
+ import { changeBlockType } from "@reiwuzen/blocky";
338
+ import { jsx as jsx3 } from "react/jsx-runtime";
339
+ var BLOCK_TYPES = [
340
+ { type: "paragraph", label: "Text" },
341
+ { type: "heading1", label: "H1" },
342
+ { type: "heading2", label: "H2" },
343
+ { type: "heading3", label: "H3" },
344
+ { type: "bullet", label: "Bullet" },
345
+ { type: "number", label: "Number" },
346
+ { type: "todo", label: "Todo" },
347
+ { type: "code", label: "Code" },
348
+ { type: "equation", label: "Equation" }
349
+ ];
350
+ function BlockTypeSwitcher({ block, onClose }) {
351
+ const { updateBlock } = useEditorActions();
352
+ return /* @__PURE__ */ jsx3("div", { className: "blocky-type-switcher", children: BLOCK_TYPES.map(({ type, label }) => /* @__PURE__ */ jsx3(
353
+ "button",
354
+ {
355
+ className: `blocky-type-option ${block.type === type ? "blocky-type-option--active" : ""}`,
356
+ onMouseDown: (e) => {
357
+ e.preventDefault();
358
+ changeBlockType(block, type).match(
359
+ (b) => {
360
+ updateBlock(b);
361
+ onClose();
362
+ },
363
+ () => {
364
+ }
365
+ );
366
+ },
367
+ children: label
368
+ },
369
+ type
370
+ )) });
371
+ }
372
+
373
+ // src/components/toolbar/FormatToolbar.tsx
374
+ import { useEffect, useRef, useState } from "react";
375
+ import { toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough, toggleHighlight, toggleColor, flatToSelection } from "@reiwuzen/blocky";
376
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
377
+ function FormatToolbar({ block }) {
378
+ const ref = useRef(null);
379
+ const [pos, setPos] = useState(null);
380
+ const { updateBlock } = useEditorActions();
381
+ useEffect(() => {
382
+ const onSelectionChange = () => {
383
+ const sel = window.getSelection();
384
+ if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
385
+ setPos(null);
386
+ return;
387
+ }
388
+ const range = sel.getRangeAt(0);
389
+ const rect = range.getBoundingClientRect();
390
+ setPos({
391
+ top: rect.top + window.scrollY - 44,
392
+ left: rect.left + window.scrollX + rect.width / 2
393
+ });
394
+ };
395
+ document.addEventListener("selectionchange", onSelectionChange);
396
+ return () => document.removeEventListener("selectionchange", onSelectionChange);
397
+ }, []);
398
+ if (!pos) return null;
399
+ const applyFormat = (fn) => {
400
+ const sel = window.getSelection();
401
+ if (!sel) return;
402
+ const start = Math.min(sel.anchorOffset, sel.focusOffset);
403
+ const end = Math.max(sel.anchorOffset, sel.focusOffset);
404
+ flatToSelection(block, start, end).match(
405
+ (nodeSel) => fn(block.content, nodeSel).match(
406
+ (content) => updateBlock({ ...block, content }),
407
+ () => {
408
+ }
409
+ ),
410
+ () => {
411
+ }
412
+ );
413
+ };
414
+ return /* @__PURE__ */ jsxs2(
415
+ "div",
416
+ {
417
+ ref,
418
+ className: "blocky-format-toolbar",
419
+ style: { top: pos.top, left: pos.left },
420
+ onMouseDown: (e) => e.preventDefault(),
421
+ children: [
422
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn", onMouseDown: () => applyFormat(toggleBold), children: "B" }),
423
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(toggleItalic), children: "I" }),
424
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(toggleUnderline), children: "U" }),
425
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--strike", onMouseDown: () => applyFormat(toggleStrikethrough), children: "S" }),
426
+ /* @__PURE__ */ jsx4("div", { className: "blocky-toolbar-divider" }),
427
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--highlight", onMouseDown: () => applyFormat((n, s) => toggleHighlight(n, s, "yellow")), children: "H" }),
428
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--red", onMouseDown: () => applyFormat((n, s) => toggleColor(n, s, "red")), children: "A" }),
429
+ /* @__PURE__ */ jsx4("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--blue", onMouseDown: () => applyFormat((n, s) => toggleColor(n, s, "blue")), children: "A" })
430
+ ]
431
+ }
432
+ );
433
+ }
434
+
435
+ // src/components/block.tsx
436
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
437
+ function Block({
438
+ block,
439
+ isActive,
440
+ editable,
441
+ onFocus,
442
+ onDragStart,
443
+ onDrop,
444
+ blockRefs,
445
+ hydratedBlocks
446
+ }) {
447
+ const [showSwitcher, setShowSwitcher] = useState2(false);
448
+ const [isDragOver, setIsDragOver] = useState2(false);
449
+ const { updateBlock } = useEditorActions();
450
+ const { className, placeholder } = blockMeta(block);
451
+ return /* @__PURE__ */ jsxs3(
452
+ "div",
453
+ {
454
+ className: `blocky-block-row ${isDragOver ? "blocky-drag-over" : ""}`,
455
+ onDragOver: (e) => {
456
+ e.preventDefault();
457
+ setIsDragOver(true);
458
+ },
459
+ onDragLeave: () => setIsDragOver(false),
460
+ onDrop: (e) => {
461
+ e.preventDefault();
462
+ setIsDragOver(false);
463
+ const dragId = e.dataTransfer.getData("text/plain");
464
+ if (dragId !== block.id) onDrop(dragId, block.id);
465
+ },
466
+ children: [
467
+ editable && /* @__PURE__ */ jsxs3("div", { className: "blocky-gutter", children: [
468
+ /* @__PURE__ */ jsx5(DragHandle, { blockId: block.id, onDragStart }),
469
+ /* @__PURE__ */ jsx5(
470
+ "button",
471
+ {
472
+ className: "blocky-type-btn",
473
+ onMouseDown: (e) => {
474
+ e.preventDefault();
475
+ setShowSwitcher((v) => !v);
476
+ },
477
+ title: "Change block type",
478
+ children: "\u229E"
479
+ }
480
+ )
481
+ ] }),
482
+ /* @__PURE__ */ jsxs3("div", { className: "blocky-block-wrapper", children: [
483
+ block.type === "bullet" && /* @__PURE__ */ jsx5("span", { className: "blocky-bullet-marker", children: "\u2022" }),
484
+ block.type === "number" && /* @__PURE__ */ jsx5("span", { className: "blocky-number-marker", children: "1." }),
485
+ block.type === "todo" && /* @__PURE__ */ jsx5(
486
+ "input",
487
+ {
488
+ type: "checkbox",
489
+ className: "blocky-todo-checkbox",
490
+ checked: !!block.meta.checked,
491
+ onChange: () => {
492
+ import("@reiwuzen/blocky").then(({ toggleTodo }) => {
493
+ toggleTodo(block).match(
494
+ (b) => updateBlock(b),
495
+ () => {
496
+ }
497
+ );
498
+ });
499
+ }
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsx5(
503
+ EditableContent,
504
+ {
505
+ block,
506
+ className,
507
+ placeholder,
508
+ editable,
509
+ onFocus,
510
+ blockRefs,
511
+ hydratedBlocks
512
+ }
513
+ ),
514
+ editable && isActive && /* @__PURE__ */ jsx5(FormatToolbar, { block }),
515
+ editable && showSwitcher && /* @__PURE__ */ jsx5(BlockTypeSwitcher, { block, onClose: () => setShowSwitcher(false) })
516
+ ] })
517
+ ]
518
+ }
519
+ );
520
+ }
521
+ function blockMeta(block) {
522
+ switch (block.type) {
523
+ case "heading1":
524
+ return { className: "blocky-h1", placeholder: "Heading 1" };
525
+ case "heading2":
526
+ return { className: "blocky-h2", placeholder: "Heading 2" };
527
+ case "heading3":
528
+ return { className: "blocky-h3", placeholder: "Heading 3" };
529
+ case "bullet":
530
+ return { className: "blocky-list-content", placeholder: "List item" };
531
+ case "number":
532
+ return { className: "blocky-list-content", placeholder: "List item" };
533
+ case "todo":
534
+ return { className: "blocky-list-content", placeholder: "To-do" };
535
+ case "code":
536
+ return { className: "blocky-code-content", placeholder: "" };
537
+ case "equation":
538
+ return { className: "blocky-equation-content", placeholder: "LaTeX" };
539
+ default:
540
+ return { className: "blocky-paragraph", placeholder: "Type something..." };
541
+ }
542
+ }
543
+
544
+ // src/components/blockList.tsx
545
+ import { jsx as jsx6 } from "react/jsx-runtime";
546
+ function BlockList({ editable, blockRefs, hydratedBlocks }) {
547
+ const blocks = useBlocks();
548
+ const activeBlockId = useActiveBlockId();
549
+ const { setBlocks, setActiveBlockId } = useEditorActions();
550
+ const handleDrop = useCallback2((dragId, dropId) => {
551
+ const from = blocks.findIndex((b) => b.id === dragId);
552
+ const to = blocks.findIndex((b) => b.id === dropId);
553
+ if (from === -1 || to === -1) return;
554
+ const next = [...blocks];
555
+ const [moved] = next.splice(from, 1);
556
+ next.splice(to, 0, moved);
557
+ setBlocks(next);
558
+ }, [blocks, setBlocks]);
559
+ return /* @__PURE__ */ jsx6("div", { className: "blocky-block-list", children: blocks.map((block) => /* @__PURE__ */ jsx6(
560
+ Block,
561
+ {
562
+ block,
563
+ isActive: block.id === activeBlockId,
564
+ editable,
565
+ onFocus: setActiveBlockId,
566
+ onDragStart: setActiveBlockId,
567
+ onDrop: handleDrop,
568
+ blockRefs,
569
+ hydratedBlocks
570
+ },
571
+ block.id
572
+ )) });
573
+ }
574
+
575
+ // src/components/editor.tsx
576
+ import { jsx as jsx7 } from "react/jsx-runtime";
577
+ var EditorContext = createContext(null);
578
+ function Editor({
579
+ blocks: seedBlocks,
580
+ onChange,
581
+ editable = true,
582
+ className,
583
+ placeholder = "Start writing..."
584
+ }) {
585
+ const blockRefs = useRef3(/* @__PURE__ */ new Map());
586
+ const hydratedBlocks = useRef3(/* @__PURE__ */ new Set());
587
+ const prevEditable = useRef3(editable);
588
+ const store = useMemo2(
589
+ () => createEditorStore({ initialBlocks: seedBlocks }),
590
+ // eslint-disable-next-line react-hooks/exhaustive-deps
591
+ []
592
+ );
593
+ useLayoutEffect(() => {
594
+ const state = store.getState();
595
+ if (seedBlocks && seedBlocks.length > 0) {
596
+ state.setBlocks(seedBlocks);
597
+ } else if (state.blocks.length === 0) {
598
+ createBlock("paragraph").match(
599
+ (b) => state.setBlocks([b]),
600
+ () => {
601
+ }
602
+ );
603
+ }
604
+ }, []);
605
+ useEffect2(() => {
606
+ const wasEditable = prevEditable.current;
607
+ prevEditable.current = editable;
608
+ if (!wasEditable || editable) return;
609
+ const storeBlocks = store.getState().blocks;
610
+ const serialized = storeBlocks.map((block) => {
611
+ const el = blockRefs.current.get(block.id);
612
+ if (!el) return block;
613
+ let content;
614
+ if (block.type === "code") content = [{ type: "code", text: el.textContent ?? "" }];
615
+ else if (block.type === "equation") content = [{ type: "equation", latex: el.textContent ?? "" }];
616
+ else content = domToNodes(el);
617
+ return { ...block, content };
618
+ });
619
+ onChange?.(serialized);
620
+ }, [editable, onChange, store]);
621
+ return /* @__PURE__ */ jsx7(EditorContext.Provider, { value: store, children: /* @__PURE__ */ jsx7(
622
+ "div",
623
+ {
624
+ className: `blocky-editor ${!editable ? "blocky-editor--readonly" : ""} ${className ?? ""}`,
625
+ "data-placeholder": placeholder,
626
+ children: /* @__PURE__ */ jsx7(
627
+ BlockList,
628
+ {
629
+ editable,
630
+ blockRefs,
631
+ hydratedBlocks
632
+ }
633
+ )
634
+ }
635
+ ) });
636
+ }
637
+
638
+ // src/hooks/useSelection.ts
639
+ import { useCallback as useCallback3 } from "react";
640
+ import { flatToSelection as flatToSelection2 } from "@reiwuzen/blocky";
641
+ function useSelection(block) {
642
+ return useCallback3(() => {
643
+ const sel = window.getSelection();
644
+ if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
645
+ const start = Math.min(sel.anchorOffset, sel.focusOffset);
646
+ const end = Math.max(sel.anchorOffset, sel.focusOffset);
647
+ const result = flatToSelection2(block, start, end);
648
+ return result.ok ? result.value : null;
649
+ }, [block]);
650
+ }
651
+ export {
652
+ Block,
653
+ BlockList,
654
+ BlockTypeSwitcher,
655
+ DragHandle,
656
+ EditableContent,
657
+ Editor,
658
+ FormatToolbar,
659
+ domToNodes,
660
+ nodesToHtml,
661
+ useActiveBlockId,
662
+ useBlockKeyboard,
663
+ useBlocks,
664
+ useEditor,
665
+ useEditorActions,
666
+ useSelection
667
+ };