@barocss/editor-view-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.
Files changed (54) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +89 -0
  3. package/dist/editor-view-react/src/EditorView.d.ts +14 -0
  4. package/dist/editor-view-react/src/EditorView.d.ts.map +1 -0
  5. package/dist/editor-view-react/src/EditorViewContentLayer.d.ts +9 -0
  6. package/dist/editor-view-react/src/EditorViewContentLayer.d.ts.map +1 -0
  7. package/dist/editor-view-react/src/EditorViewContext.d.ts +43 -0
  8. package/dist/editor-view-react/src/EditorViewContext.d.ts.map +1 -0
  9. package/dist/editor-view-react/src/EditorViewLayer.d.ts +8 -0
  10. package/dist/editor-view-react/src/EditorViewLayer.d.ts.map +1 -0
  11. package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts +14 -0
  12. package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts.map +1 -0
  13. package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts +45 -0
  14. package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts.map +1 -0
  15. package/dist/editor-view-react/src/dom-sync/edit-position.d.ts +6 -0
  16. package/dist/editor-view-react/src/dom-sync/edit-position.d.ts.map +1 -0
  17. package/dist/editor-view-react/src/index.d.ts +12 -0
  18. package/dist/editor-view-react/src/index.d.ts.map +1 -0
  19. package/dist/editor-view-react/src/input-handler.d.ts +51 -0
  20. package/dist/editor-view-react/src/input-handler.d.ts.map +1 -0
  21. package/dist/editor-view-react/src/mutation-observer-manager.d.ts +13 -0
  22. package/dist/editor-view-react/src/mutation-observer-manager.d.ts.map +1 -0
  23. package/dist/editor-view-react/src/selection-handler.d.ts +56 -0
  24. package/dist/editor-view-react/src/selection-handler.d.ts.map +1 -0
  25. package/dist/editor-view-react/src/types.d.ts +103 -0
  26. package/dist/editor-view-react/src/types.d.ts.map +1 -0
  27. package/dist/index.cjs +4 -0
  28. package/dist/index.js +11882 -0
  29. package/docs/SPEC_VERIFICATION.md +109 -0
  30. package/docs/editor-view-react-spec.md +359 -0
  31. package/docs/improvement-opportunities.md +66 -0
  32. package/docs/layers-spec.md +97 -0
  33. package/package.json +53 -0
  34. package/src/EditorView.tsx +312 -0
  35. package/src/EditorViewContentLayer.tsx +90 -0
  36. package/src/EditorViewContext.tsx +228 -0
  37. package/src/EditorViewLayer.tsx +35 -0
  38. package/src/EditorViewOverlayLayerContent.tsx +42 -0
  39. package/src/dom-sync/classify-c1.ts +91 -0
  40. package/src/dom-sync/edit-position.ts +27 -0
  41. package/src/index.ts +33 -0
  42. package/src/input-handler.ts +716 -0
  43. package/src/mutation-observer-manager.ts +65 -0
  44. package/src/selection-handler.ts +450 -0
  45. package/src/types.ts +123 -0
  46. package/test/EditorView-decorator.test.tsx +198 -0
  47. package/test/EditorView-layers.test.tsx +352 -0
  48. package/test/EditorView.test.tsx +218 -0
  49. package/test/dom-sync.test.ts +49 -0
  50. package/test/mutation-observer-manager.test.ts +48 -0
  51. package/test/selection-handler.test.ts +86 -0
  52. package/tsconfig.json +12 -0
  53. package/vite.config.ts +26 -0
  54. package/vitest.config.ts +10 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * MutationObserver manager for editor-view-react.
3
+ * Observes contentEditableElement and calls onMutations (e.g. inputHandler.handleDomMutations).
4
+ * Same role as editor-view-dom's MutationObserverManagerImpl; does not depend on editor-view-dom.
5
+ */
6
+ export interface ReactMutationObserverManager {
7
+ setup(contentEditableElement: HTMLElement): void;
8
+ disconnect(): void;
9
+ }
10
+
11
+ type OnMutations = (mutations: MutationRecord[]) => void;
12
+
13
+ export function createMutationObserverManager(onMutations: OnMutations): ReactMutationObserverManager {
14
+ let observer: MutationObserver | null = null;
15
+ let pendingMutations: MutationRecord[] = [];
16
+ let mutationTimer: number | null = null;
17
+
18
+ return {
19
+ setup(contentEditableElement: HTMLElement) {
20
+ if (observer) {
21
+ observer.disconnect();
22
+ observer = null;
23
+ }
24
+ if (mutationTimer != null) {
25
+ clearTimeout(mutationTimer);
26
+ mutationTimer = null;
27
+ }
28
+ pendingMutations = [];
29
+
30
+ observer = new MutationObserver((mutations: MutationRecord[]) => {
31
+ pendingMutations.push(...mutations);
32
+ if (mutationTimer != null) clearTimeout(mutationTimer);
33
+ mutationTimer = window.setTimeout(() => {
34
+ if (pendingMutations.length > 0) {
35
+ onMutations([...pendingMutations]);
36
+ pendingMutations = [];
37
+ }
38
+ mutationTimer = null;
39
+ }, 0);
40
+ });
41
+
42
+ observer.observe(contentEditableElement, {
43
+ childList: true,
44
+ subtree: true,
45
+ characterData: true,
46
+ attributes: true,
47
+ attributeFilter: ['data-bc-edit', 'data-bc-value', 'data-bc-sid', 'data-bc-stype'],
48
+ characterDataOldValue: true,
49
+ attributeOldValue: true,
50
+ });
51
+ },
52
+
53
+ disconnect() {
54
+ if (observer) {
55
+ observer.disconnect();
56
+ observer = null;
57
+ }
58
+ if (mutationTimer != null) {
59
+ clearTimeout(mutationTimer);
60
+ mutationTimer = null;
61
+ }
62
+ pendingMutations = [];
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,450 @@
1
+ import type { Editor } from '@barocss/editor-core';
2
+ import { fromDOMSelection } from '@barocss/editor-core';
3
+ import {
4
+ buildTextRunIndex,
5
+ binarySearchRun,
6
+ type ContainerRuns,
7
+ } from '@barocss/shared';
8
+
9
+ export type ModelSelection =
10
+ | { type: 'none' }
11
+ | { type: 'range'; startNodeId: string; startOffset: number; endNodeId: string; endOffset: number; direction?: 'forward' | 'backward' | 'none' }
12
+ | { type: 'node'; nodeId: string };
13
+
14
+ /**
15
+ * Selection handler for React editor view: converts DOM Selection to/from model selection
16
+ * using renderer-dom text run index. Does not depend on editor-view-dom.
17
+ */
18
+ export class ReactSelectionHandler {
19
+ private editor: Editor;
20
+ private getContentEditableElement: () => HTMLElement | null;
21
+ private _isProgrammaticChange = false;
22
+
23
+ constructor(
24
+ editor: Editor,
25
+ getContentEditableElement: () => HTMLElement | null
26
+ ) {
27
+ this.editor = editor;
28
+ this.getContentEditableElement = getContentEditableElement;
29
+ }
30
+
31
+ setProgrammaticChange(value: boolean): void {
32
+ this._isProgrammaticChange = value;
33
+ }
34
+
35
+ /**
36
+ * Returns true if the given (or current) selection is entirely inside inline-text nodes.
37
+ * Used to restrict character input to editable text only (same as editor-view-dom).
38
+ */
39
+ isSelectionInsideEditableText(domSelection?: Selection | null): boolean {
40
+ const sel = domSelection ?? window.getSelection();
41
+ if (!sel || sel.rangeCount === 0) return false;
42
+
43
+ const contentEditable = this.getContentEditableElement();
44
+ if (!contentEditable) return false;
45
+ if (!sel.anchorNode || !contentEditable.contains(sel.anchorNode)) return false;
46
+ if (sel.focusNode && !contentEditable.contains(sel.focusNode)) return false;
47
+
48
+ const dataStore = this.editor.dataStore;
49
+ if (!dataStore?.getNode) return false;
50
+
51
+ const checkNode = (node: Node | null): boolean => {
52
+ if (!node) return false;
53
+ const el = node.nodeType === Node.TEXT_NODE ? (node.parentElement as Element | null) : (node as Element);
54
+ if (!el) return false;
55
+ const found = el.closest('[data-bc-sid]');
56
+ if (!found) return false;
57
+ const sid = found.getAttribute('data-bc-sid');
58
+ if (!sid) return false;
59
+ const modelNode = dataStore.getNode(sid);
60
+ if (!modelNode) return false;
61
+ const stype = (modelNode as { stype?: string }).stype ?? (modelNode as { type?: string }).type;
62
+ return stype === 'inline-text';
63
+ };
64
+
65
+ return checkNode(sel.anchorNode) && checkNode(sel.focusNode ?? sel.anchorNode);
66
+ }
67
+
68
+ handleSelectionChange(): void {
69
+ if (this._isProgrammaticChange) return;
70
+
71
+ const selection = window.getSelection();
72
+ if (!selection) return;
73
+
74
+ const contentEditable = this.getContentEditableElement();
75
+ if (!contentEditable) return;
76
+
77
+ const anchorNode = selection.anchorNode;
78
+ const focusNode = selection.focusNode;
79
+ if (!anchorNode) return;
80
+
81
+ const isAnchorInside = contentEditable.contains(anchorNode);
82
+ const isFocusInside = !focusNode || contentEditable.contains(focusNode);
83
+ if (!isAnchorInside || !isFocusInside) return;
84
+
85
+ let node: Node | null = anchorNode;
86
+ while (node) {
87
+ if (node instanceof Element && node.hasAttribute('data-devtool')) return;
88
+ node = node.parentNode;
89
+ }
90
+
91
+ const modelSelection = this.convertDOMSelectionToModel(selection);
92
+ this.editor.updateSelection?.(modelSelection);
93
+ }
94
+
95
+ convertDOMSelectionToModel(selection: Selection): ModelSelection {
96
+ if (selection.rangeCount === 0) return { type: 'none' };
97
+
98
+ const range = selection.getRangeAt(0);
99
+ const boundaries = this.convertRangeBoundariesToModel(
100
+ range.startContainer,
101
+ range.startOffset,
102
+ range.endContainer,
103
+ range.endOffset
104
+ );
105
+ if (!boundaries) return { type: 'none' };
106
+
107
+ const { startNodeId, startModelOffset, endNodeId, endModelOffset } = boundaries;
108
+ const startNode = this.findBestContainer(range.startContainer);
109
+ const endNode = this.findBestContainer(range.endContainer);
110
+ const direction =
111
+ startNode && endNode
112
+ ? this.determineSelectionDirection(
113
+ selection,
114
+ startNode,
115
+ endNode,
116
+ startModelOffset,
117
+ endModelOffset
118
+ )
119
+ : 'forward';
120
+
121
+ const modelSelection = fromDOMSelection(
122
+ startNodeId,
123
+ startModelOffset,
124
+ endNodeId,
125
+ endModelOffset,
126
+ 'range'
127
+ );
128
+ return { ...modelSelection, direction } as ModelSelection;
129
+ }
130
+
131
+ convertStaticRangeToModel(
132
+ staticRange: StaticRange
133
+ ): { type: 'range'; startNodeId: string; startOffset: number; endNodeId: string; endOffset: number; direction?: 'forward' } | null {
134
+ const boundaries = this.convertRangeBoundariesToModel(
135
+ staticRange.startContainer,
136
+ staticRange.startOffset,
137
+ staticRange.endContainer,
138
+ staticRange.endOffset
139
+ );
140
+ if (!boundaries) return null;
141
+
142
+ const { startNodeId, startModelOffset, endNodeId, endModelOffset } = boundaries;
143
+ return {
144
+ type: 'range',
145
+ startNodeId,
146
+ startOffset: startModelOffset,
147
+ endNodeId,
148
+ endOffset: endModelOffset,
149
+ direction: 'forward',
150
+ };
151
+ }
152
+
153
+ private convertRangeBoundariesToModel(
154
+ startContainer: Node,
155
+ startOffset: number,
156
+ endContainer: Node,
157
+ endOffset: number
158
+ ): { startNodeId: string; startModelOffset: number; endNodeId: string; endModelOffset: number } | null {
159
+ const startNode = this.findBestContainer(startContainer);
160
+ const endNode = this.findBestContainer(endContainer);
161
+
162
+ if (!startNode || !endNode) return null;
163
+
164
+ const startNodeId = startNode.getAttribute('data-bc-sid');
165
+ const endNodeId = endNode.getAttribute('data-bc-sid');
166
+
167
+ if (!startNodeId || !endNodeId) return null;
168
+ if (!this.nodeExistsInModel(startNodeId) || !this.nodeExistsInModel(endNodeId)) return null;
169
+
170
+ const startRuns = this.ensureRuns(startNode, startNodeId);
171
+ const endRuns = startNode === endNode ? startRuns : this.ensureRuns(endNode, endNodeId);
172
+
173
+ const startModelOffset = this.convertOffsetWithRuns(
174
+ startNode,
175
+ startContainer,
176
+ startOffset,
177
+ startRuns,
178
+ false
179
+ );
180
+ const endModelOffset = this.convertOffsetWithRuns(
181
+ endNode,
182
+ endContainer,
183
+ endOffset,
184
+ endRuns,
185
+ true
186
+ );
187
+
188
+ return { startNodeId, startModelOffset, endNodeId, endModelOffset };
189
+ }
190
+
191
+ private isTextContainer(element: Element): boolean {
192
+ return element.getAttribute('data-text-container') === 'true';
193
+ }
194
+
195
+ private nodeExistsInModel(nodeId: string): boolean {
196
+ try {
197
+ const ds = this.editor.dataStore;
198
+ if (ds) {
199
+ const node = ds.getNode(nodeId);
200
+ return node != null;
201
+ }
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ private findClosestDataNode(node: Node): Element | null {
209
+ if (node.nodeType === Node.ELEMENT_NODE) {
210
+ const el = node as Element;
211
+ if (el.hasAttribute('data-bc-sid')) return el;
212
+ }
213
+ let current: Element | null = node.parentElement;
214
+ while (current) {
215
+ if (current.hasAttribute('data-bc-sid')) return current;
216
+ current = current.parentElement;
217
+ }
218
+ return null;
219
+ }
220
+
221
+ private findBestContainer(node: Node): Element | null {
222
+ let el = this.findClosestDataNode(node);
223
+ if (!el) return null;
224
+
225
+ if (this.isTextContainer(el)) return el;
226
+
227
+ let cur: Element | null = el;
228
+ while (cur) {
229
+ if (this.isTextContainer(cur)) return cur;
230
+ cur = cur.parentElement?.closest?.('[data-bc-sid]') ?? null;
231
+ }
232
+
233
+ const sid = el.getAttribute('data-bc-sid');
234
+ if (sid) {
235
+ const model = this.editor.dataStore?.getNode?.(sid);
236
+ if ((model as { stype?: string })?.stype === 'document') return null;
237
+ }
238
+ return el;
239
+ }
240
+
241
+ private ensureRuns(containerEl: Element, containerId: string): ContainerRuns {
242
+ return buildTextRunIndex(containerEl, containerId, {
243
+ buildReverseMap: true,
244
+ excludePredicate: (el) => el.hasAttribute('data-bc-decorator'),
245
+ });
246
+ }
247
+
248
+ private convertOffsetWithRuns(
249
+ containerEl: Element,
250
+ container: Node,
251
+ offset: number,
252
+ runs: ContainerRuns,
253
+ isEnd: boolean
254
+ ): number {
255
+ if (runs.total === 0) return 0;
256
+ if (container.nodeType === Node.TEXT_NODE) {
257
+ const textNode = container as Text;
258
+ const entry = runs.byNode?.get(textNode);
259
+ if (entry) {
260
+ const localLen = entry.end - entry.start;
261
+ const clamped = Math.max(0, Math.min(offset, localLen));
262
+ return entry.start + clamped;
263
+ }
264
+ const idx = binarySearchRun(runs.runs, Math.max(0, Math.min(offset, runs.total - 1)));
265
+ if (idx >= 0) return isEnd ? runs.runs[idx].end : runs.runs[idx].start;
266
+ return 0;
267
+ }
268
+ const el = container as Element;
269
+ const boundaryText = this.findTextAtElementBoundary(containerEl, el, offset, isEnd);
270
+ if (boundaryText) {
271
+ const entry = runs.byNode?.get(boundaryText);
272
+ if (entry) return isEnd ? entry.end : entry.start;
273
+ }
274
+ return isEnd ? runs.total : 0;
275
+ }
276
+
277
+ private findTextAtElementBoundary(
278
+ containerEl: Element,
279
+ el: Element,
280
+ offset: number,
281
+ isEnd: boolean
282
+ ): Text | null {
283
+ const walker = document.createTreeWalker(containerEl, NodeFilter.SHOW_TEXT);
284
+ const child = el.childNodes.item(offset) ?? null;
285
+ let lastBefore: Text | null = null;
286
+ let firstAtOrAfter: Text | null = null;
287
+ let t = walker.nextNode() as Text | null;
288
+ while (t) {
289
+ if (child) {
290
+ const pos = (t as Node).compareDocumentPosition(child);
291
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
292
+ firstAtOrAfter = t;
293
+ break;
294
+ }
295
+ lastBefore = t;
296
+ } else {
297
+ lastBefore = t;
298
+ }
299
+ t = walker.nextNode() as Text | null;
300
+ }
301
+ return isEnd ? (lastBefore ?? firstAtOrAfter) : (firstAtOrAfter ?? lastBefore);
302
+ }
303
+
304
+ private determineSelectionDirection(
305
+ selection: Selection,
306
+ startNode: Element,
307
+ endNode: Element,
308
+ startOffset: number,
309
+ endOffset: number
310
+ ): 'forward' | 'backward' {
311
+ if (startNode === endNode) return startOffset <= endOffset ? 'forward' : 'backward';
312
+
313
+ const anchorNode = selection.anchorNode;
314
+ const focusNode = selection.focusNode;
315
+ if (!anchorNode || !focusNode) {
316
+ const position = startNode.compareDocumentPosition(endNode);
317
+ return position & Node.DOCUMENT_POSITION_FOLLOWING ? 'forward' : 'backward';
318
+ }
319
+
320
+ const anchorContainer = this.findBestContainer(anchorNode);
321
+ const focusContainer = this.findBestContainer(focusNode);
322
+ if (anchorContainer && focusContainer) {
323
+ const startNodeId = startNode.getAttribute('data-bc-sid');
324
+ const endNodeId = endNode.getAttribute('data-bc-sid');
325
+ const anchorId = anchorContainer.getAttribute('data-bc-sid');
326
+ const focusId = focusContainer.getAttribute('data-bc-sid');
327
+ if (anchorId === startNodeId && focusId === endNodeId) return 'forward';
328
+ if (anchorId === endNodeId && focusId === startNodeId) return 'backward';
329
+ }
330
+
331
+ const position = startNode.compareDocumentPosition(endNode);
332
+ return position & Node.DOCUMENT_POSITION_FOLLOWING ? 'forward' : 'backward';
333
+ }
334
+
335
+ convertModelSelectionToDOM(modelSelection: ModelSelection | null | undefined): void {
336
+ this._isProgrammaticChange = true;
337
+ try {
338
+ if (!modelSelection || modelSelection.type === 'none') {
339
+ window.getSelection()?.removeAllRanges();
340
+ return;
341
+ }
342
+ if (modelSelection.type === 'range') {
343
+ this.convertRangeSelectionToDOM(modelSelection);
344
+ } else if (modelSelection.type === 'node') {
345
+ this.convertNodeSelectionToDOM(modelSelection);
346
+ }
347
+ } finally {
348
+ setTimeout(() => {
349
+ this._isProgrammaticChange = false;
350
+ }, 0);
351
+ }
352
+ }
353
+
354
+ private convertRangeSelectionToDOM(rangeSelection: {
355
+ startNodeId: string;
356
+ startOffset: number;
357
+ endNodeId: string;
358
+ endOffset: number;
359
+ }): void {
360
+ const { startNodeId, startOffset, endNodeId, endOffset } = rangeSelection;
361
+
362
+ const startElementRaw = document.querySelector(`[data-bc-sid="${startNodeId}"]`);
363
+ const endElementRaw = document.querySelector(`[data-bc-sid="${endNodeId}"]`);
364
+ if (!startElementRaw || !endElementRaw) return;
365
+
366
+ const startElement = this.findBestContainer(startElementRaw);
367
+ const endElement = this.findBestContainer(endElementRaw);
368
+ if (!startElement || !endElement) return;
369
+
370
+ const startRuns = this.getTextRunsForContainer(startElement);
371
+ const endRuns = this.getTextRunsForContainer(endElement);
372
+
373
+ let startRange = startRuns?.runs?.length
374
+ ? this.findDOMRangeFromModelOffset(startRuns, startOffset)
375
+ : null;
376
+ let endRange = endRuns?.runs?.length
377
+ ? this.findDOMRangeFromModelOffset(endRuns, endOffset)
378
+ : null;
379
+
380
+ if (!startRange) {
381
+ startRange = { node: startElementRaw, offset: Math.min(startOffset, startElementRaw.childNodes.length) };
382
+ }
383
+ if (!endRange) {
384
+ endRange = { node: endElementRaw, offset: Math.min(endOffset, endElementRaw.childNodes.length) };
385
+ }
386
+
387
+ const selection = window.getSelection();
388
+ if (!selection) return;
389
+
390
+ selection.removeAllRanges();
391
+ const range = document.createRange();
392
+ range.setStart(startRange.node, startRange.offset);
393
+ range.setEnd(endRange.node, endRange.offset);
394
+ selection.addRange(range);
395
+ }
396
+
397
+ private convertNodeSelectionToDOM(nodeSelection: { nodeId: string }): void {
398
+ const element = document.querySelector(`[data-bc-sid="${nodeSelection.nodeId}"]`);
399
+ if (!element) return;
400
+
401
+ const selection = window.getSelection();
402
+ if (!selection) return;
403
+
404
+ selection.removeAllRanges();
405
+ const range = document.createRange();
406
+ range.selectNodeContents(element);
407
+ selection.addRange(range);
408
+ }
409
+
410
+ private getTextRunsForContainer(container: Element): ContainerRuns | null {
411
+ try {
412
+ const containerId = container.getAttribute('data-bc-sid');
413
+ return buildTextRunIndex(container, containerId ?? undefined, {
414
+ buildReverseMap: true,
415
+ excludePredicate: (el) =>
416
+ el.hasAttribute('data-decorator-sid') ||
417
+ el.hasAttribute('data-bc-decorator') ||
418
+ el.hasAttribute('data-decorator-category'),
419
+ normalizeWhitespace: false,
420
+ });
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ private findDOMRangeFromModelOffset(
427
+ runs: ContainerRuns,
428
+ modelOffset: number
429
+ ): { node: Node; offset: number } | null {
430
+ if (modelOffset < 0 || modelOffset > runs.total) return null;
431
+
432
+ if (modelOffset === runs.total) {
433
+ const lastRun = runs.runs[runs.runs.length - 1];
434
+ return {
435
+ node: lastRun.domTextNode,
436
+ offset: lastRun.domTextNode.textContent?.length ?? 0,
437
+ };
438
+ }
439
+
440
+ const runIndex = binarySearchRun(runs.runs, modelOffset);
441
+ if (runIndex === -1) return null;
442
+
443
+ const run = runs.runs[runIndex];
444
+ const localOffset = modelOffset - run.start;
445
+ return {
446
+ node: run.domTextNode,
447
+ offset: Math.min(localOffset, run.domTextNode.textContent?.length ?? 0),
448
+ };
449
+ }
450
+ }
package/src/types.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { Editor } from '@barocss/editor-core';
2
+ import type { RendererRegistry } from '@barocss/dsl';
3
+ import type {
4
+ Decorator,
5
+ DecoratorGenerator,
6
+ DecoratorManager,
7
+ RemoteDecoratorManager,
8
+ PatternDecoratorConfigManager,
9
+ DecoratorGeneratorManager,
10
+ DecoratorExportData,
11
+ LoadDecoratorsPatternFunctions,
12
+ DecoratorQueryOptions,
13
+ DecoratorTypeSchema,
14
+ } from '@barocss/shared';
15
+
16
+ export type { DecoratorExportData, LoadDecoratorsPatternFunctions, DecoratorQueryOptions, DecoratorTypeSchema };
17
+
18
+ /** Model selection type for convert* APIs. */
19
+ export type ModelSelection =
20
+ | { type: 'none' }
21
+ | {
22
+ type: 'range';
23
+ startNodeId: string;
24
+ startOffset: number;
25
+ endNodeId: string;
26
+ endOffset: number;
27
+ direction?: 'forward' | 'backward' | 'none';
28
+ }
29
+ | { type: 'node'; nodeId: string };
30
+
31
+ /** Imperative handle for EditorView: decorator management and selection/convenience APIs. */
32
+ export interface EditorViewHandle {
33
+ addDecorator(decorator: Decorator | DecoratorGenerator): void;
34
+ removeDecorator(id: string): void;
35
+ updateDecorator(id: string, updates: Partial<Decorator>): void;
36
+ getDecorators(options?: DecoratorQueryOptions): Decorator[];
37
+ getDecorator(id: string): Decorator | undefined;
38
+ exportDecorators(): DecoratorExportData;
39
+ loadDecorators(data: DecoratorExportData, patternFunctions?: LoadDecoratorsPatternFunctions): void;
40
+ /** Content-editable root element (null until mounted). */
41
+ contentEditableElement: HTMLElement | null;
42
+ convertModelSelectionToDOM(sel: ModelSelection | null | undefined): void;
43
+ convertDOMSelectionToModel(selection: Selection): ModelSelection;
44
+ /** Converts a StaticRange (e.g. from getRangeAt) to model selection, or null if not resolvable. */
45
+ convertStaticRangeToModel(staticRange: StaticRange): ModelSelection | null;
46
+ defineDecoratorType(type: string, category: 'layer' | 'inline' | 'block', schema: DecoratorTypeSchema): void;
47
+ decoratorManager: DecoratorManager | null;
48
+ remoteDecoratorManager: RemoteDecoratorManager | null;
49
+ patternDecoratorConfigManager: PatternDecoratorConfigManager | null;
50
+ decoratorGeneratorManager: DecoratorGeneratorManager | null;
51
+ }
52
+
53
+ export type EditorViewLayerType = 'decorator' | 'selection' | 'context' | 'custom';
54
+
55
+ /** Options for the content layer (document rendering). Decorators are managed internally; use ref.addDecorator / ref.getDecorators. */
56
+ export interface EditorViewContentLayerOptions {
57
+ /** Renderer registry. If omitted, uses getGlobalRegistry(). */
58
+ registry?: RendererRegistry;
59
+ /** Class name for the contenteditable wrapper. */
60
+ className?: string;
61
+ /** Whether the content is editable. Default true. */
62
+ editable?: boolean;
63
+ }
64
+
65
+ /** Options for overlay layers (decorator, selection, context, custom). */
66
+ export interface EditorViewLayerOptions {
67
+ /** Class name for the layer wrapper. */
68
+ className?: string;
69
+ /** Inline styles. */
70
+ style?: React.CSSProperties;
71
+ }
72
+
73
+ /** Layer configuration for EditorView (optional per-layer classNames/styles). */
74
+ export interface EditorViewLayersConfig {
75
+ content?: EditorViewContentLayerOptions;
76
+ decorator?: EditorViewLayerOptions;
77
+ selection?: EditorViewLayerOptions;
78
+ context?: EditorViewLayerOptions;
79
+ custom?: EditorViewLayerOptions;
80
+ }
81
+
82
+ export interface EditorViewOptions {
83
+ /** Renderer registry (used by content layer if layers.content not set). */
84
+ registry?: RendererRegistry;
85
+ /** Class name for the root container. */
86
+ className?: string;
87
+ /** Per-layer configuration. */
88
+ layers?: EditorViewLayersConfig;
89
+ }
90
+
91
+ export interface EditorViewProps {
92
+ /** Editor instance. */
93
+ editor: Editor;
94
+ /** Optional options (registry, className, layers). */
95
+ options?: EditorViewOptions;
96
+ /** Optional children (e.g. custom layer content). Rendered inside the custom layer slot when present. */
97
+ children?: React.ReactNode;
98
+ }
99
+
100
+ /** EditorView ref type (when using forwardRef). Exposes decorator API like editor-view-dom. */
101
+ export type EditorViewRef = EditorViewHandle;
102
+
103
+ export interface EditorViewContentLayerProps {
104
+ /** Options (registry, className, editable). Editor is taken from EditorViewContext only. */
105
+ options?: EditorViewContentLayerOptions;
106
+ }
107
+
108
+ export interface EditorViewLayerProps {
109
+ /** Layer type (data-bc-layer value). */
110
+ layer: EditorViewLayerType;
111
+ /** Optional className. */
112
+ className?: string;
113
+ /** Optional style. */
114
+ style?: React.CSSProperties;
115
+ children?: React.ReactNode;
116
+ }
117
+
118
+ /** Props for named overlay layers (DecoratorLayer, SelectionLayer, ContextLayer, CustomLayer). */
119
+ export interface EditorViewOverlayLayerProps {
120
+ className?: string;
121
+ style?: React.CSSProperties;
122
+ children?: React.ReactNode;
123
+ }