@synclineapi/mdx-editor 0.1.2 → 1.0.1

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 (56) hide show
  1. package/README.md +403 -139
  2. package/dist/core/config/EditorConfig.d.ts +5 -0
  3. package/dist/core/config/defaults.d.ts +33 -0
  4. package/dist/core/config/index.d.ts +2 -0
  5. package/dist/core/dom/editor-pane.d.ts +7 -0
  6. package/dist/core/dom/editor-root.d.ts +8 -0
  7. package/dist/core/dom/index.d.ts +11 -0
  8. package/dist/core/dom/preview-pane.d.ts +5 -0
  9. package/dist/core/dom/skip-link.d.ts +1 -0
  10. package/dist/core/dom/status-bar.d.ts +13 -0
  11. package/dist/core/dom/toolbar-dom.d.ts +5 -0
  12. package/dist/core/editor.d.ts +139 -7
  13. package/dist/core/mdx-themes.d.ts +3 -0
  14. package/dist/core/platform.d.ts +31 -0
  15. package/dist/core/plugin-manager.d.ts +1 -24
  16. package/dist/core/plugins/PluginContext.d.ts +26 -0
  17. package/dist/core/plugins/PluginManager.d.ts +41 -0
  18. package/dist/core/plugins/index.d.ts +3 -0
  19. package/dist/core/renderer/CodeRenderer.d.ts +4 -0
  20. package/dist/core/renderer/ListRenderer.d.ts +5 -0
  21. package/dist/core/renderer/MarkdownRenderer.d.ts +7 -0
  22. package/dist/core/renderer/MdxValidator.d.ts +3 -0
  23. package/dist/core/renderer/Renderer.d.ts +10 -0
  24. package/dist/core/renderer/TableRenderer.d.ts +4 -0
  25. package/dist/core/renderer/index.d.ts +7 -0
  26. package/dist/core/renderer/sanitize.d.ts +1 -0
  27. package/dist/core/renderer.d.ts +6 -31
  28. package/dist/core/toolbar.d.ts +1 -0
  29. package/dist/core/types.d.ts +150 -2
  30. package/dist/core/ui/AutocompleteController.d.ts +58 -0
  31. package/dist/core/ui/DropdownController.d.ts +8 -0
  32. package/dist/core/ui/FindReplaceController.d.ts +60 -0
  33. package/dist/core/ui/LineNumberController.d.ts +218 -0
  34. package/dist/core/ui/ModeController.d.ts +9 -0
  35. package/dist/core/ui/PreviewController.d.ts +13 -0
  36. package/dist/core/ui/ResponsiveController.d.ts +30 -0
  37. package/dist/core/ui/SplitterController.d.ts +31 -0
  38. package/dist/core/ui/StatusBarController.d.ts +16 -0
  39. package/dist/core/ui/ThemeController.d.ts +8 -0
  40. package/dist/core/ui/ToolbarController.d.ts +10 -0
  41. package/dist/core/ui/UIController.d.ts +43 -0
  42. package/dist/core/ui/index.d.ts +12 -0
  43. package/dist/index.d.ts +29 -1
  44. package/dist/plugins/basic-formatting/index.d.ts +1 -0
  45. package/dist/plugins/callouts/index.d.ts +1 -0
  46. package/dist/plugins/diagrams/index.d.ts +1 -0
  47. package/dist/plugins/index.d.ts +1 -11
  48. package/dist/plugins/layout/index.d.ts +3 -0
  49. package/dist/plugins/lists/index.d.ts +1 -0
  50. package/dist/plugins/media/index.d.ts +2 -0
  51. package/dist/plugins/token-utils.d.ts +49 -0
  52. package/dist/plugins/utilities/index.d.ts +5 -0
  53. package/dist/style.css +1 -1
  54. package/dist/syncline-mdx-editor.js +9628 -1835
  55. package/dist/syncline-mdx-editor.umd.cjs +988 -90
  56. package/package.json +5 -4
@@ -1,3 +1,7 @@
1
+ import { CompletionItem, CompletionKind, TokenSegment } from '@synclineapi/editor';
2
+ export type { CompletionItem, CompletionKind, TokenSegment };
3
+ /** Function signature for a plugin-level syntax token provider. */
4
+ export type PluginTokenProvider = (line: string, language: string) => TokenSegment[];
1
5
  export interface EditorConfig {
2
6
  container: HTMLElement | string;
3
7
  /** Initial markdown content */
@@ -14,8 +18,6 @@ export interface EditorConfig {
14
18
  placeholder?: string;
15
19
  /** Read-only mode */
16
20
  readOnly?: boolean;
17
- /** Enable scroll sync between editor and preview */
18
- scrollSync?: boolean;
19
21
  /** Custom renderer overrides */
20
22
  renderers?: Record<string, RendererFn>;
21
23
  /** Locale / i18n overrides */
@@ -47,6 +49,59 @@ export interface EditorPlugin {
47
49
  styles?: string;
48
50
  /** Plugin dependencies (names) */
49
51
  dependencies?: string[];
52
+ /**
53
+ * Autocomplete items this plugin contributes to the code editor.
54
+ *
55
+ * These are merged with the built-in MDX completions so that anything
56
+ * a plugin can insert via the toolbar is also reachable via autocomplete.
57
+ *
58
+ * Use `kind: 'snip'` with a `body` template to get Tab-trigger snippet
59
+ * expansion that matches the toolbar insertion exactly.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * completions: [
64
+ * {
65
+ * label: 'myblock',
66
+ * kind: 'snip',
67
+ * detail: '<MyBlock> component',
68
+ * description: 'Inserts a MyBlock component with optional title.',
69
+ * body: '<MyBlock title="$1Title">\n $2Content here\n</MyBlock>',
70
+ * },
71
+ * ]
72
+ * ```
73
+ */
74
+ completions?: CompletionItem[];
75
+ /**
76
+ * Custom syntax token provider for this plugin's MDX constructs.
77
+ *
78
+ * Called on every visible line in the code editor **in addition to** the
79
+ * built-in MDX tokeniser. Return `TokenSegment` objects for the character
80
+ * ranges you want coloured. Ranges not covered fall back to the base MDX
81
+ * tokeniser.
82
+ *
83
+ * All plugin providers are composed into a single `provideTokens` function
84
+ * that is passed to the underlying `SynclineEditor`. The result is LRU-
85
+ * cached by the editor, so the function is only called once per unique
86
+ * `(text, language)` pair.
87
+ *
88
+ * Use `ctx.registerTokenProvider()` inside `init()` when the segments
89
+ * need to be computed dynamically (e.g. based on runtime config).
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * provideTokens: (line, _lang) => {
94
+ * const segs: TokenSegment[] = [];
95
+ * // Highlight <MyBlock component name
96
+ * for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
97
+ * const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
98
+ * segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
99
+ * }
100
+ * return segs;
101
+ * }
102
+ * ```
103
+ */
104
+ provideTokens?: PluginTokenProvider;
50
105
  }
51
106
  export interface PluginContext {
52
107
  /** The editor instance */
@@ -61,6 +116,43 @@ export interface PluginContext {
61
116
  registerParser(parser: ParserConfig): void;
62
117
  /** Inject CSS */
63
118
  injectStyles(css: string): void;
119
+ /**
120
+ * Register an autocomplete completion item for the code editor.
121
+ * Equivalent to declaring `completions` on the plugin definition, but
122
+ * useful when the items are built dynamically inside `init()`.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * ctx.registerCompletion({
127
+ * label: 'myblock',
128
+ * kind: 'snip',
129
+ * detail: '<MyBlock> component',
130
+ * body: '<MyBlock title="$1">\n $2\n</MyBlock>',
131
+ * });
132
+ * ```
133
+ */
134
+ registerCompletion(item: CompletionItem): void;
135
+ /**
136
+ * Register a dynamic token provider for this plugin's MDX syntax.
137
+ * Equivalent to declaring `provideTokens` on the plugin definition but
138
+ * useful when the provider needs to be built dynamically inside `init()`.
139
+ *
140
+ * Multiple providers registered from the same plugin are all called and
141
+ * their results merged into the final token list.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * ctx.registerTokenProvider((line, _lang) => {
146
+ * const segs: TokenSegment[] = [];
147
+ * for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
148
+ * const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
149
+ * segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
150
+ * }
151
+ * return segs;
152
+ * });
153
+ * ```
154
+ */
155
+ registerTokenProvider(fn: PluginTokenProvider): void;
64
156
  /** Emit a plugin event */
65
157
  emit(event: string, data?: unknown): void;
66
158
  /** Listen to events */
@@ -166,6 +258,62 @@ export interface EditorAPI {
166
258
  getWordCount(): number;
167
259
  /** Get line count */
168
260
  getLineCount(): number;
261
+ /** Show or hide the line-number gutter (virtual rendering) */
262
+ setLineNumbers(enabled: boolean): void;
263
+ /** Scroll the editor to make the given 1-indexed line visible */
264
+ jumpToLine(lineNumber: number): void;
265
+ /**
266
+ * Register one or more custom autocomplete items on top of the built-in
267
+ * MDX completions. Items are merged immediately and appear in the popup
268
+ * the next time the user types.
269
+ *
270
+ * Use `kind: 'snip'` with a `body` template for snippet expansion — the
271
+ * Tab key or clicking the item inserts the full body with cursor placed
272
+ * at the first `$1` tab stop.
273
+ *
274
+ * Call multiple times to accumulate items. The full list (built-ins +
275
+ * plugin items + items registered here) is always deduplicated by label.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * editor.registerAutoComplete([
280
+ * {
281
+ * label: 'mycard',
282
+ * kind: 'snip',
283
+ * detail: '<MyCard> component',
284
+ * description: 'Inserts a custom card block.',
285
+ * body: '<MyCard title="$1">\n $2\n</MyCard>',
286
+ * },
287
+ * { label: 'MyCard', kind: 'cls', detail: 'component' },
288
+ * ]);
289
+ * ```
290
+ */
291
+ registerAutoComplete(items: CompletionItem | CompletionItem[]): void;
292
+ /**
293
+ * Register one or more custom syntax token providers on top of the
294
+ * built-in MDX tokeniser and any plugin-contributed providers.
295
+ *
296
+ * Each provider is called on every visible line in the code editor and
297
+ * may return `TokenSegment` objects covering the character ranges that
298
+ * should receive custom syntax colouring. The result is LRU-cached by
299
+ * the underlying `SynclineEditor`.
300
+ *
301
+ * Equivalent to declaring `provideTokens` on a custom `EditorPlugin` —
302
+ * use whichever is more convenient.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * editor.registerSyntaxHighlighter((line, _lang) => {
307
+ * const segs: TokenSegment[] = [];
308
+ * for (const m of line.matchAll(/<\/?( MyBlock)(?=[\s>/])/g)) {
309
+ * const s = m.index! + 1 + (m[0][1] === '/' ? 1 : 0);
310
+ * segs.push({ cls: 'cls', start: s, end: s + 'MyBlock'.length });
311
+ * }
312
+ * return segs;
313
+ * });
314
+ * ```
315
+ */
316
+ registerSyntaxHighlighter(fn: PluginTokenProvider | PluginTokenProvider[]): void;
169
317
  }
170
318
  export interface SelectionState {
171
319
  start: number;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * AutocompleteController — MDX/Markdown-aware autocomplete popup.
3
+ *
4
+ * Adapted from the vreditor.html autocomplete system. Shows suggestions for:
5
+ * - MDX component tags (Accordion, Card, Tabs, etc.)
6
+ * - Markdown syntax (headings, bold, lists, code blocks, etc.)
7
+ * - HTML tags
8
+ * - Document-based word completions (words already present in the editor)
9
+ *
10
+ * The popup is positioned relative to the cursor and supports keyboard
11
+ * navigation (Arrow Up/Down, Tab/Enter to accept, Escape to dismiss).
12
+ */
13
+ /** A single autocomplete suggestion. */
14
+ export interface AutocompleteItem {
15
+ /** Display label shown in the popup */
16
+ label: string;
17
+ /** Category kind — drives the badge icon */
18
+ kind: 'tag' | 'md' | 'html' | 'var' | 'snippet';
19
+ /** Short description shown next to label */
20
+ detail: string;
21
+ /** Text to insert (defaults to label if omitted) */
22
+ insertText?: string;
23
+ }
24
+ export declare class AutocompleteController {
25
+ private popup;
26
+ private items;
27
+ private selected;
28
+ private prefix;
29
+ private startPos;
30
+ private textarea;
31
+ private editorRoot;
32
+ private _onAccept;
33
+ /** Additional items registered at runtime by plugins. */
34
+ private extraItems;
35
+ constructor(textarea: HTMLTextAreaElement, editorRoot: HTMLElement);
36
+ /** Register additional completion items (e.g. from plugins). */
37
+ registerItems(items: AutocompleteItem[]): void;
38
+ /** Whether the popup is currently visible. */
39
+ isVisible(): boolean;
40
+ /** Trigger autocomplete based on current cursor context. */
41
+ trigger(): void;
42
+ /** Move selection up or down. */
43
+ move(dir: number): void;
44
+ /** Accept the current selection. */
45
+ accept(): void;
46
+ /** Set a callback to fire after an item is accepted (for triggering onInput). */
47
+ onAccept(fn: () => void): void;
48
+ /** Handle keyboard events. Returns true if the event was consumed. */
49
+ handleKeyDown(e: KeyboardEvent): boolean;
50
+ /** Hide the autocomplete popup. */
51
+ hide(): void;
52
+ /** Render the popup items. */
53
+ private render;
54
+ /** Position the popup near the cursor. */
55
+ private position;
56
+ /** Clean up DOM. */
57
+ destroy(): void;
58
+ }
@@ -0,0 +1,8 @@
1
+ export declare class DropdownController {
2
+ private activeDropdown;
3
+ private documentClickHandler;
4
+ constructor();
5
+ toggle(menu: HTMLElement, wrapper: HTMLElement): void;
6
+ closeAll(): void;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * FindReplaceController — Find & Replace panel for the editor.
3
+ *
4
+ * Adapted from the vreditor.html find/replace system. Provides:
5
+ * - Ctrl+F to open find, Ctrl+H to open find+replace
6
+ * - Case-sensitive toggle
7
+ * - Regex search toggle
8
+ * - Whole-word match toggle
9
+ * - Navigate between matches (Enter / Shift+Enter or arrows)
10
+ * - Replace current / Replace all
11
+ * - Escape to close
12
+ */
13
+ export declare class FindReplaceController {
14
+ private panel;
15
+ private findInput;
16
+ private replaceInput;
17
+ private replaceRow;
18
+ private countLabel;
19
+ private caseSensitiveBtn;
20
+ private regexBtn;
21
+ private wholeWordBtn;
22
+ private textarea;
23
+ private editorRoot;
24
+ private matches;
25
+ private currentMatch;
26
+ private caseSensitive;
27
+ private useRegex;
28
+ private wholeWord;
29
+ private showReplace;
30
+ private _onInputCallback;
31
+ constructor(textarea: HTMLTextAreaElement, editorRoot: HTMLElement);
32
+ /** Set a callback to fire after a replace operation (for triggering onInput). */
33
+ onInput(fn: () => void): void;
34
+ /** Open the find panel. If `withReplace` is true, also show the replace row. */
35
+ open(withReplace?: boolean): void;
36
+ /** Close the find panel. */
37
+ close(): void;
38
+ /** Whether the panel is open. */
39
+ isOpen(): boolean;
40
+ /** Run the search. */
41
+ private search;
42
+ /** Go to next match. */
43
+ private next;
44
+ /** Go to previous match. */
45
+ private prev;
46
+ /** Select the current match in the textarea. */
47
+ private highlightCurrent;
48
+ /** Update the match count label. */
49
+ private updateCount;
50
+ /** Replace the current match. */
51
+ private replaceCurrent;
52
+ /** Replace all matches. */
53
+ private replaceAll;
54
+ /** Create a toggle button. */
55
+ private createToggle;
56
+ /** Create an action button. */
57
+ private createActionBtn;
58
+ /** Clean up. */
59
+ destroy(): void;
60
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Virtual line-number gutter — unified scroll container approach.
3
+ *
4
+ * Only ~(visible lines + 2×OVERSCAN) DOM nodes live in the DOM at any time,
5
+ * regardless of document size. For a 10,000-line file only ~40 <span> elements
6
+ * are ever created; a 100-line file and a 10,000-line file feel identical.
7
+ *
8
+ * Layout model (unified scroll container):
9
+ *
10
+ * .smdx-scroll-wrapper overflow-y:auto — SINGLE scroll container
11
+ * ├─ .smdx-line-gutter flex-shrink:0, position:relative
12
+ * │ └─ .smdx-gutter-inner position:relative, height = totalHeight
13
+ * │ └─ <span> position:absolute, top = lineOffset + paddingTop
14
+ * └─ <textarea> flex:1, overflow:hidden, height auto-sized
15
+ *
16
+ * The gutter and textarea are flex siblings inside a shared scroll wrapper.
17
+ * When the wrapper scrolls, both children scroll together naturally — like
18
+ * a flex row per line. No JS-based transform synchronisation is needed.
19
+ *
20
+ * Virtual rendering:
21
+ * • recyclePool() computes the visible range from the scroll wrapper's
22
+ * scrollTop and clientHeight.
23
+ * • If the range hasn't changed (OVERSCAN has buffered all visible lines),
24
+ * it returns immediately — zero DOM reads, zero DOM writes.
25
+ * • Only when lines enter or leave the overscan region are a handful of
26
+ * span properties updated (style.top + textContent).
27
+ *
28
+ * Multi-line wrapping:
29
+ * • For each logical line, rowCount() measures how many visual rows it takes
30
+ * using a hidden mirror div with identical font/width settings.
31
+ * • heights[i] = rowCount(i) × lineHeightPx (full multi-row height stored).
32
+ * • The line-number span for line i is only 1 row tall — pinned to the first
33
+ * visual row (VS Code convention). offsets[] accounts for the full height
34
+ * so successive numbers are correctly spaced.
35
+ */
36
+ /** Public snapshot of the gutter's virtual-rendering state. */
37
+ export interface GutterStats {
38
+ /** Number of logical lines in the document. */
39
+ lineCount: number;
40
+ /** 1-indexed first line currently in the DOM pool. */
41
+ visibleFirst: number;
42
+ /** 1-indexed last line currently in the DOM pool. */
43
+ visibleLast: number;
44
+ /** Number of <span> elements currently in the DOM. */
45
+ domNodes: number;
46
+ /** Computed total content height (px). */
47
+ totalHeight: number;
48
+ /** Whether the gutter / virtual renderer is enabled. */
49
+ enabled: boolean;
50
+ }
51
+ export declare class LineNumberController {
52
+ private gutter;
53
+ private textarea;
54
+ /** The shared scroll wrapper (parent of gutter + textarea). */
55
+ private scrollEl;
56
+ private enabled;
57
+ private mirror;
58
+ private rafPending;
59
+ private scrollRafId;
60
+ private offsets;
61
+ private heights;
62
+ private totalHeight;
63
+ private lineCount;
64
+ private lineHeightPx;
65
+ private contentWidth;
66
+ private avgCharWidth;
67
+ private paddingTop;
68
+ private cachedClientHeight;
69
+ private static readonly WRAP_THRESHOLD;
70
+ /**
71
+ * Documents above this line count skip per-line mirror-div measurement
72
+ * in the synchronous path and use pure character-width estimation instead.
73
+ * This keeps large pastes (2000+ lines) instant while keeping the gutter
74
+ * correct for typical editing sessions.
75
+ */
76
+ private static readonly LARGE_DOC_THRESHOLD;
77
+ /**
78
+ * Delay (ms) before running the deferred precision pass for large documents.
79
+ * Short enough to feel immediate, long enough to let paste events settle.
80
+ */
81
+ private static readonly DEFERRED_UPDATE_DELAY_MS;
82
+ /**
83
+ * Minimum content-width change (px) to trigger a full geometry recompute
84
+ * on resize. Smaller changes are ignored to avoid unnecessary recalculation.
85
+ */
86
+ private static readonly WIDTH_CHANGE_THRESHOLD_PX;
87
+ /**
88
+ * Debounce timer for deferring heavy mirror-div measurement on large
89
+ * content changes (paste, setValue with 2000+ lines).
90
+ */
91
+ private deferredUpdateTimer;
92
+ /**
93
+ * Cached content width used to detect width changes on resize so we
94
+ * can trigger a full geometry recompute (wrapping changes).
95
+ */
96
+ private cachedContentWidth;
97
+ /** Cached textarea scrollHeight for optimised autoResizeTextarea(). */
98
+ private lastTextareaHeight;
99
+ private inner;
100
+ /**
101
+ * OVERSCAN — extra lines rendered above and below the visible area.
102
+ * 8 gives a comfortable buffer for fast momentum scrolling; the span-recycling
103
+ * approach (no DOM teardown) means a larger overscan has negligible cost.
104
+ */
105
+ private readonly OVERSCAN;
106
+ private spanPool;
107
+ private poolStart;
108
+ private poolEnd;
109
+ /** Called after every pool rebuild — use to drive live status-bar stats. */
110
+ onViewportChange: ((stats: GutterStats) => void) | null;
111
+ constructor(gutter: HTMLElement, textarea: HTMLTextAreaElement);
112
+ /**
113
+ * Set the shared scroll wrapper element.
114
+ * Must be called before enable() so scroll events are bound to the wrapper.
115
+ */
116
+ setScrollElement(el: HTMLElement): void;
117
+ private buildInner;
118
+ private setupMirror;
119
+ private destroyMirror;
120
+ /**
121
+ * Copy font metrics from the textarea into the mirror div.
122
+ * Returns computed layout values, or null when layout is unavailable
123
+ * (e.g. jsdom / hidden element).
124
+ */
125
+ private syncMirror;
126
+ /**
127
+ * Return the number of visual rows occupied by `line`.
128
+ * Fast path: if the line clearly fits on one row (no wrap), return 1
129
+ * without touching the mirror div. Only wrapping lines pay the
130
+ * mirror-div measurement cost.
131
+ */
132
+ private rowCount;
133
+ enable(): void;
134
+ disable(): void;
135
+ /**
136
+ * Refresh cached layout dimensions after the editor container is resized.
137
+ * Call this from a ResizeObserver on the editor pane or the scroll wrapper
138
+ * so that recyclePool() continues using accurate viewport dimensions
139
+ * without reading from the DOM on every scroll event.
140
+ *
141
+ * If the content width has changed (e.g. splitter drag, window resize),
142
+ * this triggers a full geometry recompute because wrapping may have changed.
143
+ */
144
+ notifyResize(): void;
145
+ /**
146
+ * Full recompute: called whenever the document content changes.
147
+ * Measures each line's visual height and rebuilds the geometry cache,
148
+ * then re-renders the visible viewport.
149
+ *
150
+ * For large documents (> LARGE_DOC_THRESHOLD lines), this uses fast
151
+ * character-width estimation instead of per-line mirror-div measurement.
152
+ * A deferred rAF pass can refine wrapping estimates if needed.
153
+ */
154
+ update(): void;
155
+ /**
156
+ * Deferred precision pass for large documents: uses the mirror div
157
+ * only for lines that likely wrap, then updates the geometry cache.
158
+ * Runs after a short delay so paste operations are not blocked.
159
+ */
160
+ private refineLargeDoc;
161
+ /**
162
+ * Auto-resize the textarea so its full content is visible (no internal
163
+ * scrollbar). The shared scroll wrapper provides the single scrollbar
164
+ * for both gutter and textarea.
165
+ *
166
+ * Optimised: only resets height when the content has likely changed size,
167
+ * avoiding unnecessary layout thrashing on every keystroke.
168
+ */
169
+ autoResizeTextarea(): void;
170
+ /**
171
+ * Recycle the span pool for the current viewport.
172
+ * Called after content changes and synchronously on every scroll event.
173
+ */
174
+ private renderVisible;
175
+ /**
176
+ * Recycle the span pool for the new visible range.
177
+ *
178
+ * Spans are updated in-place (style.top + textContent) — zero DOM
179
+ * add/remove during scrolling. This runs SYNCHRONOUSLY in the scroll
180
+ * handler and maintains 60fps even for 10,000+ line documents.
181
+ *
182
+ * With the unified scroll wrapper the gutter and textarea share the
183
+ * same scrollTop so no scroll-fraction mapping is needed.
184
+ */
185
+ private recyclePool;
186
+ /**
187
+ * Binary search: return the index of the logical line whose range
188
+ * contains or immediately precedes `offsetPx`.
189
+ */
190
+ private lineIndexAtOffset;
191
+ /**
192
+ * Scroll handler — fully synchronous, no rAF deferral needed.
193
+ *
194
+ * With the unified scroll wrapper, this simply reads scrollTop from
195
+ * the wrapper and recycles the span pool. No transform writes are
196
+ * needed since both gutter and textarea scroll together naturally.
197
+ */
198
+ private onScroll;
199
+ private cancelScrollRaf;
200
+ /**
201
+ * Ensure the cursor's line is visible within the scroll wrapper.
202
+ * Call after input, keyup, or any operation that may move the cursor.
203
+ */
204
+ ensureCursorVisible(): void;
205
+ /**
206
+ * Return the content-space Y offset (px) for the given 0-indexed logical line.
207
+ * Used by the editor's jumpToLine() to scroll the textarea precisely.
208
+ */
209
+ getLineOffset(lineIndex: number): number;
210
+ /**
211
+ * Return a snapshot of the virtual renderer's current state.
212
+ * Useful for live status-bar displays.
213
+ */
214
+ getStats(): GutterStats;
215
+ /** Return the scroll wrapper element, if set. */
216
+ getScrollElement(): HTMLElement | null;
217
+ destroy(): void;
218
+ }
@@ -0,0 +1,9 @@
1
+ import { EditorMode } from '../types';
2
+ export declare class ModeController {
3
+ private currentMode;
4
+ private root;
5
+ constructor(root: HTMLElement, initialMode?: EditorMode);
6
+ getMode(): EditorMode;
7
+ setMode(mode: EditorMode): void;
8
+ private applyMode;
9
+ }
@@ -0,0 +1,13 @@
1
+ import { RendererConfig, ParserConfig } from '../types';
2
+ export declare class PreviewController {
3
+ private renderer;
4
+ private contentEl;
5
+ private renderTimer;
6
+ private delay;
7
+ constructor(contentEl: HTMLElement, delay?: number);
8
+ setRenderers(renderers: RendererConfig[]): void;
9
+ setParsers(parsers: ParserConfig[]): void;
10
+ render(source: string): void;
11
+ renderImmediate(source: string): Promise<void>;
12
+ destroy(): void;
13
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ResponsiveController
3
+ *
4
+ * Watches the editor root element's width via ResizeObserver and applies
5
+ * `smdx-is-mobile` / `smdx-is-tablet` CSS classes so that responsive
6
+ * styles work correctly for embedded editors whose width is independent
7
+ * of the viewport width.
8
+ *
9
+ * Breakpoints (mirrors the CSS @media queries in mobile.css):
10
+ * ≤ 640 px → smdx-is-mobile
11
+ * 641–1024 px → smdx-is-tablet
12
+ * > 1024 px → (no extra class)
13
+ */
14
+ export declare class ResponsiveController {
15
+ private root;
16
+ private observer;
17
+ /** Breakpoint widths in pixels – must match mobile.css */
18
+ private static readonly MOBILE_BREAKPOINT;
19
+ private static readonly TABLET_BREAKPOINT;
20
+ constructor(root: HTMLElement);
21
+ /**
22
+ * Start observing. Call once after the editor root is in the DOM.
23
+ */
24
+ start(): void;
25
+ /**
26
+ * Stop observing and clean up.
27
+ */
28
+ destroy(): void;
29
+ private applyBreakpointClasses;
30
+ }
@@ -0,0 +1,31 @@
1
+ export declare class SplitterController {
2
+ private splitter;
3
+ private left;
4
+ private right;
5
+ private container;
6
+ private isDragging;
7
+ private startX;
8
+ private startLeftWidth;
9
+ /** Percentage of container width to move per keyboard arrow press. */
10
+ private static readonly KEYBOARD_STEP_PERCENT;
11
+ /** Callback fired during drag so the editor can refresh line-number geometry. */
12
+ private onResizeCallback;
13
+ /** Throttle timer for resize callbacks during drag. */
14
+ private resizeRafId;
15
+ constructor(container: HTMLElement, left: HTMLElement, right: HTMLElement);
16
+ /** Set a callback to fire during drag (for refreshing line-number geometry). */
17
+ onResize(fn: () => void): void;
18
+ private onMouseDown;
19
+ private onMouseMove;
20
+ private onMouseUp;
21
+ private onTouchStart;
22
+ private onTouchMove;
23
+ private onTouchEnd;
24
+ private onKeyDown;
25
+ private startDrag;
26
+ private moveDrag;
27
+ private endDrag;
28
+ /** Throttled resize notification via rAF. */
29
+ private notifyResize;
30
+ destroy(): void;
31
+ }
@@ -0,0 +1,16 @@
1
+ import { EditorMode } from '../types';
2
+ export interface StatusBarControllerOptions {
3
+ bar: HTMLElement;
4
+ wordCount: HTMLElement;
5
+ lineCount: HTMLElement;
6
+ charCount: HTMLElement;
7
+ modeSwitcher: HTMLElement;
8
+ onModeChange: (mode: EditorMode) => void;
9
+ }
10
+ export declare class StatusBarController {
11
+ private opts;
12
+ constructor(opts: StatusBarControllerOptions);
13
+ private buildModeSwitcher;
14
+ updateCounts(text: string): void;
15
+ updateActiveMode(mode: EditorMode): void;
16
+ }
@@ -0,0 +1,8 @@
1
+ export declare class ThemeController {
2
+ private root;
3
+ private _isDark;
4
+ constructor(root: HTMLElement);
5
+ applyTheme(theme: Record<string, string>): void;
6
+ toggleDarkMode(dark?: boolean): void;
7
+ isDarkMode(): boolean;
8
+ }
@@ -0,0 +1,10 @@
1
+ import { ToolbarConfig, EditorAPI } from '../types';
2
+ import { PluginManager } from '../plugins/PluginManager';
3
+ import { EventEmitter } from '../events';
4
+ export declare class ToolbarController {
5
+ private toolbar;
6
+ constructor(config: ToolbarConfig, pluginManager: PluginManager, editorApi: EditorAPI, events: EventEmitter);
7
+ render(container: HTMLElement): HTMLElement;
8
+ updateActiveStates(): void;
9
+ destroy(): void;
10
+ }