@owomark/react 0.1.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,908 @@
1
+ // src/OwoMarkEditor.tsx
2
+ import {
3
+ useRef as useRef3,
4
+ useEffect as useEffect2,
5
+ useImperativeHandle,
6
+ forwardRef,
7
+ useCallback
8
+ } from "react";
9
+ import { getThemeClassName } from "@owomark/view";
10
+
11
+ // src/editor/EditorBlock.tsx
12
+ import { memo as memo3, useMemo } from "react";
13
+ import { BLOCK_TYPE_TO_CLASS, BQ_STEP, buildBlockquoteBarsBoxShadow } from "@owomark/core";
14
+
15
+ // src/editor/EditorDecorator.tsx
16
+ import { memo as memo2 } from "react";
17
+ import { TOKEN_TO_CLASS } from "@owomark/core";
18
+
19
+ // src/editor/EditorLeaf.tsx
20
+ import { memo } from "react";
21
+ import { jsx } from "react/jsx-runtime";
22
+ var EditorLeaf = memo(function EditorLeaf2({ text }) {
23
+ return /* @__PURE__ */ jsx("span", { "data-owo-leaf": "true", children: text });
24
+ });
25
+
26
+ // src/editor/EditorDecorator.tsx
27
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
28
+ var EditorDecorator = memo2(function EditorDecorator2({ decorator }) {
29
+ const cls = TOKEN_TO_CLASS[decorator.type] || "";
30
+ const leaves = decorator.leaves.map((leaf, i) => /* @__PURE__ */ jsx2(EditorLeaf, { text: leaf.text }, i));
31
+ if (cls) {
32
+ return /* @__PURE__ */ jsx2("span", { className: cls, "data-owo-token": decorator.type, children: leaves });
33
+ }
34
+ return /* @__PURE__ */ jsx2(Fragment, { children: leaves });
35
+ });
36
+
37
+ // src/editor/EditorBlock.tsx
38
+ import { jsx as jsx3 } from "react/jsx-runtime";
39
+ function buildBlockquoteBarsStyle(depth) {
40
+ const shadow = buildBlockquoteBarsBoxShadow(depth);
41
+ if (!shadow) return void 0;
42
+ return {
43
+ paddingLeft: `${depth * BQ_STEP}px`,
44
+ boxShadow: shadow
45
+ };
46
+ }
47
+ var EditorBlock = memo3(
48
+ function EditorBlock2({ block, blockIndex }) {
49
+ const className = BLOCK_TYPE_TO_CLASS[block.type] || "owo-block-paragraph";
50
+ const headingLevel = block.type === "heading" ? block.headingLevel : void 0;
51
+ const bqStyle = useMemo(
52
+ () => block.type === "blockquote" ? buildBlockquoteBarsStyle(block.depth) : void 0,
53
+ [block.type, block.depth]
54
+ );
55
+ const hasContent = block.decorators.length > 0 && block.decorators.some((d) => d.leaves.some((l) => l.text.length > 0));
56
+ return /* @__PURE__ */ jsx3(
57
+ "div",
58
+ {
59
+ "data-owo-block": blockIndex,
60
+ "data-block-id": block.id,
61
+ className,
62
+ style: bqStyle,
63
+ ...headingLevel ? { "data-owo-heading": headingLevel } : {},
64
+ children: hasContent ? block.decorators.map((dec, i) => /* @__PURE__ */ jsx3(EditorDecorator, { decorator: dec }, i)) : /* @__PURE__ */ jsx3("br", {})
65
+ }
66
+ );
67
+ },
68
+ (prev, next) => prev.block === next.block && prev.blockIndex === next.blockIndex
69
+ );
70
+
71
+ // src/slash/SlashMenu.tsx
72
+ import {
73
+ useRef,
74
+ useEffect,
75
+ useLayoutEffect,
76
+ useState,
77
+ memo as memo4
78
+ } from "react";
79
+
80
+ // src/slash/caret-position.ts
81
+ var MENU_GAP = 4;
82
+ function getCaretRect() {
83
+ const sel = window.getSelection();
84
+ if (!sel || sel.rangeCount === 0) return null;
85
+ const range = sel.getRangeAt(0).cloneRange();
86
+ range.collapse(true);
87
+ const rect = range.getBoundingClientRect();
88
+ if (rect.width === 0 && rect.height === 0 && rect.top === 0) return null;
89
+ return { top: rect.top, left: rect.left, bottom: rect.bottom };
90
+ }
91
+ function computeMenuPosition(caret, menuHeight, menuWidth) {
92
+ const viewportHeight = window.innerHeight;
93
+ const viewportWidth = window.innerWidth;
94
+ let top;
95
+ if (caret.bottom + MENU_GAP + menuHeight <= viewportHeight) {
96
+ top = caret.bottom + MENU_GAP;
97
+ } else {
98
+ top = caret.top - MENU_GAP - menuHeight;
99
+ }
100
+ let left = caret.left;
101
+ if (left + menuWidth > viewportWidth - 8) {
102
+ left = viewportWidth - menuWidth - 8;
103
+ }
104
+ if (left < 8) left = 8;
105
+ return { top: Math.max(0, top), left };
106
+ }
107
+
108
+ // src/slash/SlashMenu.tsx
109
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
110
+ var MENU_MAX_HEIGHT = 320;
111
+ var MENU_WIDTH = 280;
112
+ var SlashMenu = memo4(function SlashMenu2({
113
+ state,
114
+ onSelect,
115
+ onDismiss
116
+ }) {
117
+ const menuRef = useRef(null);
118
+ const [position, setPosition] = useState(null);
119
+ useLayoutEffect(() => {
120
+ if (!state.active && !state.pending) {
121
+ setPosition(null);
122
+ return;
123
+ }
124
+ let frameId = 0;
125
+ const updatePosition = () => {
126
+ const caret = getCaretRect();
127
+ if (!caret) return false;
128
+ setPosition(computeMenuPosition(caret, MENU_MAX_HEIGHT, MENU_WIDTH));
129
+ return true;
130
+ };
131
+ if (!updatePosition()) {
132
+ frameId = window.requestAnimationFrame(() => {
133
+ updatePosition();
134
+ });
135
+ }
136
+ return () => {
137
+ if (frameId) {
138
+ window.cancelAnimationFrame(frameId);
139
+ }
140
+ };
141
+ }, [state.active, state.pending, state.query]);
142
+ useEffect(() => {
143
+ if (!state.active && !state.pending || !menuRef.current) return;
144
+ const selected = menuRef.current.querySelector('[data-selected="true"]');
145
+ if (selected) {
146
+ selected.scrollIntoView({ block: "nearest" });
147
+ }
148
+ }, [state.active, state.pending, state.selectedIndex]);
149
+ useEffect(() => {
150
+ if (!state.active) return;
151
+ function handlePointerDown(e) {
152
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
153
+ onDismiss();
154
+ }
155
+ }
156
+ document.addEventListener("pointerdown", handlePointerDown, true);
157
+ return () => document.removeEventListener("pointerdown", handlePointerDown, true);
158
+ }, [state.active, onDismiss]);
159
+ if (!state.active && !state.pending || !position) return null;
160
+ const groups = groupByCategory(state.filteredCommands);
161
+ return /* @__PURE__ */ jsxs(
162
+ "div",
163
+ {
164
+ ref: menuRef,
165
+ className: "owo-slash-menu",
166
+ role: "listbox",
167
+ style: {
168
+ position: "fixed",
169
+ top: position.top,
170
+ left: position.left,
171
+ width: MENU_WIDTH,
172
+ maxHeight: MENU_MAX_HEIGHT,
173
+ overflow: "auto",
174
+ zIndex: 9999
175
+ },
176
+ children: [
177
+ state.pending && /* @__PURE__ */ jsx4("div", { className: "owo-slash-menu__pending", children: "Executing..." }),
178
+ groups.map(({ category, commands }) => /* @__PURE__ */ jsxs("div", { className: "owo-slash-menu__group", children: [
179
+ category && /* @__PURE__ */ jsx4("div", { className: "owo-slash-menu__category", children: category }),
180
+ commands.map((cmd) => {
181
+ const globalIndex = state.filteredCommands.indexOf(cmd);
182
+ const isSelected = globalIndex === state.selectedIndex;
183
+ return /* @__PURE__ */ jsxs(
184
+ "div",
185
+ {
186
+ className: `owo-slash-menu__item${isSelected ? " owo-slash-menu__item--selected" : ""}`,
187
+ role: "option",
188
+ "aria-selected": isSelected,
189
+ "data-selected": isSelected ? "true" : void 0,
190
+ onPointerDown: (e) => {
191
+ e.preventDefault();
192
+ onSelect(globalIndex);
193
+ },
194
+ children: [
195
+ cmd.icon && /* @__PURE__ */ jsx4("span", { className: "owo-slash-menu__icon", children: cmd.icon }),
196
+ /* @__PURE__ */ jsxs("div", { className: "owo-slash-menu__content", children: [
197
+ /* @__PURE__ */ jsx4("span", { className: "owo-slash-menu__label", children: cmd.label }),
198
+ cmd.shortcut && /* @__PURE__ */ jsx4("span", { className: "owo-slash-menu__shortcut", children: cmd.shortcut }),
199
+ cmd.description && /* @__PURE__ */ jsx4("span", { className: "owo-slash-menu__desc", children: cmd.description })
200
+ ] })
201
+ ]
202
+ },
203
+ cmd.id
204
+ );
205
+ })
206
+ ] }, category))
207
+ ]
208
+ }
209
+ );
210
+ });
211
+ function groupByCategory(commands) {
212
+ const groups = [];
213
+ const seen = /* @__PURE__ */ new Map();
214
+ for (const cmd of commands) {
215
+ const cat = cmd.category ?? "";
216
+ let group = seen.get(cat);
217
+ if (!group) {
218
+ group = { category: cat, commands: [] };
219
+ seen.set(cat, group);
220
+ groups.push(group);
221
+ }
222
+ group.commands.push(cmd);
223
+ }
224
+ return groups;
225
+ }
226
+
227
+ // src/useOwoMarkCore.ts
228
+ import {
229
+ useRef as useRef2,
230
+ useState as useState2,
231
+ useLayoutEffect as useLayoutEffect2
232
+ } from "react";
233
+ import {
234
+ createOwoMarkCore,
235
+ readSelection,
236
+ restoreSelection,
237
+ invalidateBlockCache,
238
+ virtualToLinear
239
+ } from "@owomark/core";
240
+ function useOwoMarkCore(options) {
241
+ const { initialMarkdown, readOnly = false } = options;
242
+ const containerRef = useRef2(null);
243
+ const coreRef = useRef2(null);
244
+ const composingRef = useRef2(false);
245
+ const pendingSelectionRef = useRef2(null);
246
+ const onCompositionStateChangeRef = useRef2(options.onCompositionStateChange);
247
+ onCompositionStateChangeRef.current = options.onCompositionStateChange;
248
+ if (!coreRef.current) {
249
+ coreRef.current = createOwoMarkCore({ initialMarkdown });
250
+ }
251
+ const core = coreRef.current;
252
+ const [doc, setDoc] = useState2(() => core.getDocument());
253
+ const [slashState, setSlashState] = useState2(() => core.getSlashState());
254
+ useLayoutEffect2(() => {
255
+ const el = containerRef.current;
256
+ if (!el) return;
257
+ const root = el;
258
+ root.contentEditable = readOnly ? "false" : "true";
259
+ root.setAttribute("role", "textbox");
260
+ root.setAttribute("aria-multiline", "true");
261
+ root.setAttribute("aria-label", "Markdown editor");
262
+ root.classList.add("owo-editor-root");
263
+ root.style.whiteSpace = "pre-wrap";
264
+ root.style.wordBreak = "break-word";
265
+ root.style.outline = "none";
266
+ const unsubDoc = core.onDocumentChange((newDoc, newSelection) => {
267
+ if (composingRef.current) return;
268
+ setDoc(newDoc);
269
+ pendingSelectionRef.current = newSelection;
270
+ });
271
+ const unsubComp = core.onCompositionStateChange((active) => {
272
+ composingRef.current = active;
273
+ onCompositionStateChangeRef.current?.(active);
274
+ });
275
+ const unsubSlash = core.onSlashStateChange((s) => setSlashState(s));
276
+ function syncEmptyAttr() {
277
+ const d = core.getDocument();
278
+ const empty = d.blocks.length === 0 || d.blocks.length === 1 && d.blocks[0].raw === "";
279
+ if (empty) root.setAttribute("data-owo-empty", "true");
280
+ else root.removeAttribute("data-owo-empty");
281
+ }
282
+ syncEmptyAttr();
283
+ let compositionJustEnded = false;
284
+ let pendingNativeSync = false;
285
+ function syncFromDOM() {
286
+ const parts = [];
287
+ for (const child of root.childNodes) {
288
+ if (child.nodeType === Node.ELEMENT_NODE) {
289
+ const el2 = child;
290
+ if (el2.hasAttribute("data-owo-block")) {
291
+ if (parts.length > 0) parts.push("\n");
292
+ parts.push(el2.textContent ?? "");
293
+ }
294
+ }
295
+ }
296
+ const textContent = parts.join("");
297
+ if (textContent !== core.getMarkdown()) {
298
+ const sel = readSelection(root);
299
+ core.syncText(textContent, sel);
300
+ syncEmptyAttr();
301
+ }
302
+ }
303
+ function onBeforeInput(e) {
304
+ if (core.getComposition().active) return;
305
+ const sel = readSelection(root);
306
+ const result = core.applyBeforeInput(
307
+ { inputType: e.inputType, data: e.data },
308
+ sel
309
+ );
310
+ if (result.action === "handled") {
311
+ e.preventDefault();
312
+ syncEmptyAttr();
313
+ } else if (result.action === "allow-native") {
314
+ pendingNativeSync = true;
315
+ }
316
+ }
317
+ function onInput() {
318
+ if (core.getComposition().active) return;
319
+ if (compositionJustEnded) {
320
+ compositionJustEnded = false;
321
+ syncFromDOM();
322
+ } else if (pendingNativeSync) {
323
+ pendingNativeSync = false;
324
+ syncFromDOM();
325
+ }
326
+ }
327
+ function onKeyDown(e) {
328
+ if (core.getComposition().active) return;
329
+ const sel = readSelection(root);
330
+ const result = core.applyKeyDown(
331
+ {
332
+ key: e.key,
333
+ ctrlKey: e.ctrlKey,
334
+ metaKey: e.metaKey,
335
+ altKey: e.altKey,
336
+ shiftKey: e.shiftKey
337
+ },
338
+ sel
339
+ );
340
+ if (result.action === "handled") {
341
+ e.preventDefault();
342
+ syncEmptyAttr();
343
+ }
344
+ }
345
+ function onPaste(e) {
346
+ e.preventDefault();
347
+ const raw = e.clipboardData?.getData("text/plain") ?? "";
348
+ const sel = readSelection(root);
349
+ core.applyPaste({ text: raw }, sel);
350
+ syncEmptyAttr();
351
+ }
352
+ function onCompositionStart() {
353
+ const sel = readSelection(root);
354
+ core.handleCompositionStart(sel);
355
+ }
356
+ function onCompositionUpdate() {
357
+ core.handleCompositionUpdate();
358
+ }
359
+ function onCompositionEnd() {
360
+ const sel = readSelection(root);
361
+ core.handleCompositionEnd(sel);
362
+ compositionJustEnded = true;
363
+ requestAnimationFrame(() => {
364
+ if (compositionJustEnded) {
365
+ compositionJustEnded = false;
366
+ syncFromDOM();
367
+ }
368
+ });
369
+ }
370
+ function onSelectionChange() {
371
+ const sel = readSelection(root);
372
+ if (sel) core.updateSelection(sel);
373
+ }
374
+ root.addEventListener("beforeinput", onBeforeInput);
375
+ root.addEventListener("input", onInput);
376
+ root.addEventListener("compositionstart", onCompositionStart);
377
+ root.addEventListener("compositionupdate", onCompositionUpdate);
378
+ root.addEventListener("compositionend", onCompositionEnd);
379
+ root.addEventListener("keydown", onKeyDown);
380
+ root.addEventListener("paste", onPaste);
381
+ const onDocSelectionChange = () => onSelectionChange();
382
+ root.ownerDocument.addEventListener("selectionchange", onDocSelectionChange);
383
+ return () => {
384
+ unsubDoc();
385
+ unsubComp();
386
+ unsubSlash();
387
+ root.removeEventListener("beforeinput", onBeforeInput);
388
+ root.removeEventListener("input", onInput);
389
+ root.removeEventListener("compositionstart", onCompositionStart);
390
+ root.removeEventListener("compositionupdate", onCompositionUpdate);
391
+ root.removeEventListener("compositionend", onCompositionEnd);
392
+ root.removeEventListener("keydown", onKeyDown);
393
+ root.removeEventListener("paste", onPaste);
394
+ root.ownerDocument.removeEventListener("selectionchange", onDocSelectionChange);
395
+ core.destroy();
396
+ };
397
+ }, []);
398
+ useLayoutEffect2(() => {
399
+ const el = containerRef.current;
400
+ if (!el || !pendingSelectionRef.current) return;
401
+ const linearSel = virtualToLinear(doc, pendingSelectionRef.current);
402
+ invalidateBlockCache(el);
403
+ restoreSelection(el, linearSel);
404
+ pendingSelectionRef.current = null;
405
+ }, [doc]);
406
+ return { core, doc, slashState, containerRef };
407
+ }
408
+
409
+ // src/config.ts
410
+ var DEFAULT_EDITOR_CONFIG = {
411
+ indentMode: "auto",
412
+ enableSideAnnotation: true,
413
+ enableMath: true,
414
+ enableMarkdownSandbox: true,
415
+ markdownSandbox: {
416
+ outerFenceTicks: 4
417
+ },
418
+ theme: "light"
419
+ };
420
+ function resolveEditorConfig(config) {
421
+ return {
422
+ ...DEFAULT_EDITOR_CONFIG,
423
+ ...config,
424
+ markdownSandbox: {
425
+ ...DEFAULT_EDITOR_CONFIG.markdownSandbox,
426
+ ...config?.markdownSandbox
427
+ }
428
+ };
429
+ }
430
+ function deriveProcessorOptions(config) {
431
+ return {
432
+ enableSideAnnotation: config.enableSideAnnotation,
433
+ enableMath: config.enableMath,
434
+ enableMarkdownSandbox: config.enableMarkdownSandbox,
435
+ markdownSandbox: config.markdownSandbox
436
+ };
437
+ }
438
+
439
+ // src/OwoMarkEditor.tsx
440
+ import "@owomark/view/style.css";
441
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
442
+ var OwoMarkEditor = forwardRef(
443
+ function OwoMarkEditor2(props, ref) {
444
+ const {
445
+ value,
446
+ defaultValue,
447
+ onChange,
448
+ onSelectionChange,
449
+ onCompositionStateChange,
450
+ onScroll,
451
+ readOnly = false,
452
+ placeholder,
453
+ className,
454
+ theme: themeProp = "light",
455
+ themeClassName,
456
+ commandsRef,
457
+ coreRef: externalCoreRef,
458
+ controller,
459
+ indentMode: indentModeProp = "auto",
460
+ ariaLabel,
461
+ config: configProp
462
+ } = props;
463
+ const resolved = resolveEditorConfig(configProp);
464
+ const indentMode = configProp ? resolved.indentMode : indentModeProp;
465
+ const theme = configProp ? resolved.theme : themeProp;
466
+ const enableSideAnnotation = resolved.enableSideAnnotation;
467
+ const enableMath = resolved.enableMath;
468
+ const isControlled = value !== void 0;
469
+ const lastExternalValue = useRef3(value ?? defaultValue ?? "");
470
+ const suppressOnChange = useRef3(false);
471
+ const { core, doc, slashState, containerRef } = useOwoMarkCore({
472
+ initialMarkdown: lastExternalValue.current,
473
+ readOnly,
474
+ onCompositionStateChange
475
+ });
476
+ const handleSlashSelect = useCallback((index) => {
477
+ core.executeSlashCommand(index);
478
+ }, [core]);
479
+ const handleSlashDismiss = useCallback(() => {
480
+ core.dismissSlashMenu();
481
+ }, [core]);
482
+ useEffect2(() => {
483
+ if (typeof externalCoreRef === "function") externalCoreRef(core);
484
+ else if (externalCoreRef && typeof externalCoreRef === "object") {
485
+ externalCoreRef.current = core;
486
+ }
487
+ return () => {
488
+ if (typeof externalCoreRef === "function") externalCoreRef(null);
489
+ else if (externalCoreRef && typeof externalCoreRef === "object") {
490
+ externalCoreRef.current = null;
491
+ }
492
+ };
493
+ }, [core, externalCoreRef]);
494
+ useEffect2(() => {
495
+ const unsub = core.onChange((markdown) => {
496
+ if (suppressOnChange.current) return;
497
+ lastExternalValue.current = markdown;
498
+ onChange?.(markdown);
499
+ });
500
+ return unsub;
501
+ }, [core, onChange]);
502
+ useEffect2(() => {
503
+ if (!onSelectionChange) return;
504
+ return core.onSelectionChange(onSelectionChange);
505
+ }, [core, onSelectionChange]);
506
+ useEffect2(() => {
507
+ if (!controller) return;
508
+ return controller.connectEditor(core);
509
+ }, [core, controller]);
510
+ useEffect2(() => {
511
+ if (!isControlled) return;
512
+ if (value === lastExternalValue.current) return;
513
+ suppressOnChange.current = true;
514
+ core.setMarkdown(value);
515
+ lastExternalValue.current = value;
516
+ suppressOnChange.current = false;
517
+ }, [core, value, isControlled]);
518
+ useEffect2(() => {
519
+ core.setIndentMode(indentMode);
520
+ }, [core, indentMode]);
521
+ useEffect2(() => {
522
+ core.setEnableSideAnnotation(enableSideAnnotation);
523
+ }, [core, enableSideAnnotation]);
524
+ useEffect2(() => {
525
+ core.setEnableMath(enableMath);
526
+ }, [core, enableMath]);
527
+ useEffect2(() => {
528
+ if (containerRef.current && ariaLabel) {
529
+ containerRef.current.setAttribute("aria-label", ariaLabel);
530
+ }
531
+ }, [ariaLabel, containerRef]);
532
+ useEffect2(() => {
533
+ if (containerRef.current) {
534
+ containerRef.current.contentEditable = readOnly ? "false" : "true";
535
+ if (readOnly) {
536
+ containerRef.current.setAttribute("aria-readonly", "true");
537
+ } else {
538
+ containerRef.current.removeAttribute("aria-readonly");
539
+ }
540
+ }
541
+ }, [readOnly, containerRef]);
542
+ useImperativeHandle(commandsRef, () => ({
543
+ focus: () => containerRef.current?.focus(),
544
+ undo: () => core.dispatch("undo"),
545
+ redo: () => core.dispatch("redo"),
546
+ toggleBold: () => core.dispatch("toggleBold"),
547
+ toggleItalic: () => core.dispatch("toggleItalic"),
548
+ insertLink: (url) => core.dispatch("insertLink", url),
549
+ insertCodeFence: (language) => core.dispatch("insertCodeFence", language),
550
+ insertMath: () => core.dispatch("insertMath"),
551
+ insertSideAnnotation: (type) => core.dispatch("insertSideAnnotation", type),
552
+ getMarkdown: () => core.getMarkdown(),
553
+ setMarkdown: (markdown) => core.setMarkdown(markdown),
554
+ replaceMarkdown: (markdown, selection) => core.replaceMarkdown(markdown, selection)
555
+ }), [core, containerRef]);
556
+ const themeClass = getThemeClassName(theme);
557
+ const combinedClassName = [
558
+ themeClass,
559
+ themeClassName,
560
+ className
561
+ ].filter(Boolean).join(" ");
562
+ const setRefs = useCallback((el) => {
563
+ containerRef.current = el;
564
+ if (typeof ref === "function") ref(el);
565
+ else if (ref) ref.current = el;
566
+ }, [ref, containerRef]);
567
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
568
+ /* @__PURE__ */ jsx5(
569
+ "div",
570
+ {
571
+ ref: setRefs,
572
+ className: combinedClassName,
573
+ "data-placeholder": placeholder,
574
+ onScroll,
575
+ suppressContentEditableWarning: true,
576
+ children: doc.blocks.map((block, i) => /* @__PURE__ */ jsx5(EditorBlock, { block, blockIndex: i }, block.id))
577
+ }
578
+ ),
579
+ /* @__PURE__ */ jsx5(
580
+ SlashMenu,
581
+ {
582
+ state: slashState,
583
+ onSelect: handleSlashSelect,
584
+ onDismiss: handleSlashDismiss
585
+ }
586
+ )
587
+ ] });
588
+ }
589
+ );
590
+
591
+ // src/OwoMarkPreview.tsx
592
+ import { useRef as useRef4, useEffect as useEffect3 } from "react";
593
+ import {
594
+ createOwoMarkPreviewEngine
595
+ } from "@owomark/view";
596
+ import { jsx as jsx6 } from "react/jsx-runtime";
597
+ function isController(store) {
598
+ return typeof store.updateVisibleBlockIds === "function";
599
+ }
600
+ var OwoMarkPreview = function OwoMarkPreview2(props) {
601
+ const {
602
+ state,
603
+ className,
604
+ strategy,
605
+ themeKey,
606
+ registry,
607
+ viewportFirst,
608
+ ariaLabel,
609
+ renderBlock,
610
+ onContentUpdate
611
+ } = props;
612
+ const onContentUpdateRef = useRef4(onContentUpdate);
613
+ onContentUpdateRef.current = onContentUpdate;
614
+ const renderBlockRef = useRef4(renderBlock);
615
+ renderBlockRef.current = renderBlock;
616
+ const hasRenderBlock = !!renderBlock;
617
+ const containerRef = useRef4(null);
618
+ const engineRef = useRef4(null);
619
+ useEffect3(() => {
620
+ if (!containerRef.current) return;
621
+ const engineOptions = {
622
+ strategy,
623
+ themeKey,
624
+ registry,
625
+ viewportFirst,
626
+ renderBlock: hasRenderBlock ? (block, ctx) => renderBlockRef.current(block, ctx) : void 0,
627
+ onContentUpdate: () => onContentUpdateRef.current?.()
628
+ };
629
+ const engine = createOwoMarkPreviewEngine(engineOptions);
630
+ engine.mount(containerRef.current);
631
+ engineRef.current = engine;
632
+ const currentState = state.getState();
633
+ void engine.update(currentState);
634
+ return () => {
635
+ engine.destroy();
636
+ engineRef.current = null;
637
+ };
638
+ }, [strategy, themeKey, registry, viewportFirst, hasRenderBlock]);
639
+ useEffect3(() => {
640
+ const unsub = state.subscribe((newState) => {
641
+ const engine = engineRef.current;
642
+ if (engine) {
643
+ void engine.update(newState);
644
+ }
645
+ });
646
+ return unsub;
647
+ }, [state]);
648
+ useEffect3(() => {
649
+ const container = containerRef.current;
650
+ if (!viewportFirst || !container || !isController(state)) return;
651
+ const controller = state;
652
+ const observer = new IntersectionObserver(
653
+ (entries) => {
654
+ const visible = /* @__PURE__ */ new Set();
655
+ for (const id of controller.getState().visibleBlockIds) {
656
+ visible.add(id);
657
+ }
658
+ for (const entry of entries) {
659
+ const blockId = entry.target.getAttribute("data-block-id");
660
+ if (!blockId) continue;
661
+ if (entry.isIntersecting) {
662
+ visible.add(blockId);
663
+ } else {
664
+ visible.delete(blockId);
665
+ }
666
+ }
667
+ controller.updateVisibleBlockIds([...visible]);
668
+ },
669
+ { root: container, rootMargin: "200px 0px" }
670
+ );
671
+ const wrappers = container.querySelectorAll("[data-block-id]");
672
+ for (const wrapper of wrappers) {
673
+ observer.observe(wrapper);
674
+ }
675
+ const mutation = new MutationObserver((mutations) => {
676
+ for (const m of mutations) {
677
+ for (const node of m.addedNodes) {
678
+ if (node instanceof HTMLElement && node.hasAttribute("data-block-id")) {
679
+ observer.observe(node);
680
+ }
681
+ }
682
+ }
683
+ });
684
+ mutation.observe(container, { childList: true, subtree: true });
685
+ return () => {
686
+ observer.disconnect();
687
+ mutation.disconnect();
688
+ };
689
+ }, [viewportFirst, state]);
690
+ return /* @__PURE__ */ jsx6(
691
+ "div",
692
+ {
693
+ ref: containerRef,
694
+ className,
695
+ role: "document",
696
+ "aria-label": ariaLabel ?? "Preview",
697
+ "aria-live": "polite"
698
+ }
699
+ );
700
+ };
701
+
702
+ // src/useOwoMarkSharedState.ts
703
+ import { useRef as useRef5, useSyncExternalStore, useCallback as useCallback2 } from "react";
704
+ import {
705
+ createSharedStateStore
706
+ } from "@owomark/core";
707
+ function useOwoMarkSharedState(options) {
708
+ const controllerRef = useRef5(null);
709
+ if (!controllerRef.current) {
710
+ controllerRef.current = createSharedStateStore(options);
711
+ }
712
+ return controllerRef.current;
713
+ }
714
+ function useSharedStateSnapshot(controller) {
715
+ const subscribe = useCallback2(
716
+ (onStoreChange) => controller.subscribe(onStoreChange),
717
+ [controller]
718
+ );
719
+ const getSnapshot = useCallback2(() => controller.getState(), [controller]);
720
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
721
+ }
722
+
723
+ // src/useBlockContext.ts
724
+ import { useState as useState3, useEffect as useEffect5 } from "react";
725
+ function useBlockContext(core) {
726
+ const [blockContext, setBlockContext] = useState3(
727
+ () => core.getBlockContext()
728
+ );
729
+ useEffect5(() => {
730
+ setBlockContext(core.getBlockContext());
731
+ return core.onBlockContextChange((ctx) => {
732
+ setBlockContext(ctx);
733
+ });
734
+ }, [core]);
735
+ return blockContext;
736
+ }
737
+
738
+ // src/index.ts
739
+ import { getThemeClassName as getThemeClassName2, THEME_LIGHT_CLASS, THEME_DARK_CLASS } from "@owomark/view";
740
+
741
+ // src/useVirtualList.ts
742
+ import { useState as useState4, useCallback as useCallback3, useRef as useRef6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
743
+ import {
744
+ buildVirtualRows,
745
+ computeVisibleRange
746
+ } from "@owomark/core";
747
+ function useVirtualList(options) {
748
+ const { blocks, containerRef, overscan = 5, charsPerLine = 80 } = options;
749
+ const heightCacheRef = useRef6(/* @__PURE__ */ new Map());
750
+ const [scrollTop, setScrollTop] = useState4(0);
751
+ const [viewportHeight, setViewportHeight] = useState4(0);
752
+ const [heightCacheVersion, setHeightCacheVersion] = useState4(0);
753
+ const rafRef = useRef6(0);
754
+ const [container, setContainer] = useState4(null);
755
+ useEffect6(() => {
756
+ setContainer(containerRef.current);
757
+ });
758
+ useEffect6(() => {
759
+ if (!container) return;
760
+ setViewportHeight(container.clientHeight);
761
+ const ro = new ResizeObserver((entries) => {
762
+ for (const entry of entries) {
763
+ setViewportHeight(entry.contentRect.height);
764
+ }
765
+ });
766
+ ro.observe(container);
767
+ return () => ro.disconnect();
768
+ }, [container]);
769
+ useEffect6(() => {
770
+ if (!container) return;
771
+ const onScroll = () => {
772
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
773
+ rafRef.current = requestAnimationFrame(() => {
774
+ setScrollTop(container.scrollTop);
775
+ });
776
+ };
777
+ container.addEventListener("scroll", onScroll, { passive: true });
778
+ return () => {
779
+ container.removeEventListener("scroll", onScroll);
780
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
781
+ };
782
+ }, [container]);
783
+ const rows = useMemo2(
784
+ () => buildVirtualRows(blocks, heightCacheRef.current, charsPerLine),
785
+ // eslint-disable-next-line react-hooks/exhaustive-deps
786
+ [blocks, charsPerLine, heightCacheVersion]
787
+ );
788
+ const visibleRange = useMemo2(
789
+ () => computeVisibleRange(rows, scrollTop, viewportHeight, overscan),
790
+ [rows, scrollTop, viewportHeight, overscan]
791
+ );
792
+ const updateBlockHeight = useCallback3((blockId, height) => {
793
+ const cache = heightCacheRef.current;
794
+ if (cache.get(blockId) !== height) {
795
+ cache.set(blockId, height);
796
+ setHeightCacheVersion((v) => v + 1);
797
+ }
798
+ }, []);
799
+ const isBlockVisible = useCallback3(
800
+ (blockIndex) => blockIndex >= visibleRange.startIndex && blockIndex <= visibleRange.endIndex,
801
+ [visibleRange]
802
+ );
803
+ const invalidateAllHeights = useCallback3(() => {
804
+ heightCacheRef.current.clear();
805
+ setHeightCacheVersion((v) => v + 1);
806
+ }, []);
807
+ return {
808
+ visibleRange,
809
+ rows,
810
+ totalHeight: visibleRange.totalHeight,
811
+ updateBlockHeight,
812
+ isBlockVisible,
813
+ invalidateAllHeights
814
+ };
815
+ }
816
+
817
+ // src/MdxSkeleton.tsx
818
+ import { jsx as jsx7 } from "react/jsx-runtime";
819
+ function MdxSkeleton({ height, lines = 3, className, style }) {
820
+ const combinedStyle = {
821
+ ...style,
822
+ ...height != null ? { height, minHeight: height } : {}
823
+ };
824
+ return /* @__PURE__ */ jsx7(
825
+ "div",
826
+ {
827
+ className: `owo-mdx-skeleton-block ${className ?? ""}`,
828
+ style: combinedStyle,
829
+ "aria-hidden": "true",
830
+ children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx7("div", { className: "owo-mdx-skeleton owo-mdx-skeleton-line" }, i))
831
+ }
832
+ );
833
+ }
834
+
835
+ // src/MdxComponentShell.tsx
836
+ import {
837
+ useRef as useRef7,
838
+ useState as useState5,
839
+ useEffect as useEffect7,
840
+ useCallback as useCallback4
841
+ } from "react";
842
+ import { jsx as jsx8 } from "react/jsx-runtime";
843
+ var componentHeightCache = /* @__PURE__ */ new Map();
844
+ var DEFAULT_ESTIMATED_HEIGHT = 80;
845
+ function MdxComponentShell({ name, children }) {
846
+ const containerRef = useRef7(null);
847
+ const [ready, setReady] = useState5(false);
848
+ const estimatedHeight = componentHeightCache.get(name) ?? DEFAULT_ESTIMATED_HEIGHT;
849
+ useEffect7(() => {
850
+ const id = requestAnimationFrame(() => setReady(true));
851
+ return () => cancelAnimationFrame(id);
852
+ }, []);
853
+ const measureHeight = useCallback4(() => {
854
+ const el = containerRef.current;
855
+ if (!el) return;
856
+ const h = el.getBoundingClientRect().height;
857
+ if (h > 0) {
858
+ componentHeightCache.set(name, h);
859
+ }
860
+ }, [name]);
861
+ useEffect7(() => {
862
+ const el = containerRef.current;
863
+ if (!el || !ready) return;
864
+ measureHeight();
865
+ const ro = new ResizeObserver(() => measureHeight());
866
+ ro.observe(el);
867
+ return () => ro.disconnect();
868
+ }, [ready, measureHeight]);
869
+ if (!ready) {
870
+ return /* @__PURE__ */ jsx8(MdxSkeleton, { height: estimatedHeight });
871
+ }
872
+ return /* @__PURE__ */ jsx8("div", { ref: containerRef, children });
873
+ }
874
+ function wrapWithShell(name, Component) {
875
+ function ShellWrapped(props) {
876
+ return /* @__PURE__ */ jsx8(MdxComponentShell, { name, children: /* @__PURE__ */ jsx8(Component, { ...props }) });
877
+ }
878
+ ShellWrapped.displayName = `MdxShell(${name})`;
879
+ return ShellWrapped;
880
+ }
881
+ function clearComponentHeightCache() {
882
+ componentHeightCache.clear();
883
+ }
884
+ export {
885
+ DEFAULT_EDITOR_CONFIG,
886
+ EditorBlock,
887
+ EditorDecorator,
888
+ EditorLeaf,
889
+ MdxComponentShell,
890
+ MdxSkeleton,
891
+ OwoMarkEditor,
892
+ OwoMarkPreview,
893
+ SlashMenu,
894
+ THEME_DARK_CLASS,
895
+ THEME_LIGHT_CLASS,
896
+ clearComponentHeightCache,
897
+ computeMenuPosition,
898
+ deriveProcessorOptions,
899
+ getCaretRect,
900
+ getThemeClassName2 as getThemeClassName,
901
+ resolveEditorConfig,
902
+ useBlockContext,
903
+ useOwoMarkCore,
904
+ useOwoMarkSharedState,
905
+ useSharedStateSnapshot,
906
+ useVirtualList,
907
+ wrapWithShell
908
+ };