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