smartrte-react 0.3.5 → 1.0.0-beta.10

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 (147) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +205 -14
  3. package/dist/adapters/domChecklistCommandBridge.js +1 -1
  4. package/dist/adapters/domCommandBridge.d.ts +2 -2
  5. package/dist/adapters/domCommandBridge.js +2 -2
  6. package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
  7. package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
  8. package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
  9. package/dist/adapters/domInlineImageCommandBridge.js +6 -72
  10. package/dist/adapters/domMoveCommandBridge.js +1 -1
  11. package/dist/adapters/domSelectionBridge.d.ts +3 -3
  12. package/dist/adapters/domSmartDocument.d.ts +4 -4
  13. package/dist/adapters/domSmartDocument.js +2 -2
  14. package/dist/adapters/domTableCommandBridge.d.ts +42 -16
  15. package/dist/adapters/domTableCommandBridge.js +203 -173
  16. package/dist/adapters/internalFlags.d.ts +0 -8
  17. package/dist/adapters/internalFlags.js +0 -24
  18. package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
  19. package/dist/adapters/legacyListShadowComparator.js +1 -1
  20. package/dist/adapters/pdfPrint.d.ts +8 -0
  21. package/dist/adapters/pdfPrint.js +22 -0
  22. package/dist/adapters/shadowMode.d.ts +3 -3
  23. package/dist/adapters/shadowMode.js +1 -1
  24. package/dist/canonicalEditorRuntime.d.ts +65 -1
  25. package/dist/canonicalEditorRuntime.js +63 -5
  26. package/dist/capabilityPresets.d.ts +27 -0
  27. package/dist/capabilityPresets.js +21 -0
  28. package/dist/collabTransport.d.ts +66 -0
  29. package/dist/collabTransport.js +1 -0
  30. package/dist/commentProvider.d.ts +24 -0
  31. package/dist/commentProvider.js +1 -0
  32. package/dist/components/CanonicalAuthorityEditor.d.ts +56 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1304 -116
  34. package/dist/components/ClassicEditorAuthority.d.ts +93 -22
  35. package/dist/components/ClassicEditorAuthority.js +11 -43
  36. package/dist/components/ColorPickerPopover.d.ts +39 -0
  37. package/dist/components/ColorPickerPopover.js +287 -0
  38. package/dist/components/CommentMarkers.d.ts +25 -0
  39. package/dist/components/CommentMarkers.js +105 -0
  40. package/dist/components/CommentThreadPanel.d.ts +26 -0
  41. package/dist/components/CommentThreadPanel.js +47 -0
  42. package/dist/components/ContextMenu.d.ts +23 -0
  43. package/dist/components/ContextMenu.js +141 -0
  44. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  45. package/dist/components/FormulaLibraryPopover.js +107 -0
  46. package/dist/components/LinkEditorPopover.d.ts +3 -1
  47. package/dist/components/LinkEditorPopover.js +22 -6
  48. package/dist/components/MediaDetailsPopover.d.ts +35 -0
  49. package/dist/components/MediaDetailsPopover.js +136 -0
  50. package/dist/components/MediaManager.d.ts +4 -20
  51. package/dist/components/MediaManager.js +40 -11
  52. package/dist/components/MediaOverlay.d.ts +27 -0
  53. package/dist/components/MediaOverlay.js +228 -0
  54. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  55. package/dist/components/SpecialCharacterPopover.js +99 -0
  56. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  57. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  58. package/dist/components/SuggestionPanel.d.ts +26 -0
  59. package/dist/components/SuggestionPanel.js +33 -0
  60. package/dist/components/TableBorderPopover.d.ts +59 -0
  61. package/dist/components/TableBorderPopover.js +149 -0
  62. package/dist/components/TableResizeHandles.d.ts +25 -0
  63. package/dist/components/TableResizeHandles.js +323 -0
  64. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  65. package/dist/components/TableSizePickerPopover.js +126 -0
  66. package/dist/components/ToolbarPrimitives.d.ts +161 -0
  67. package/dist/components/ToolbarPrimitives.js +237 -0
  68. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  69. package/dist/components/VersionHistoryPanel.js +157 -0
  70. package/dist/components/fixedPositioning.d.ts +25 -0
  71. package/dist/components/fixedPositioning.js +42 -0
  72. package/dist/formulaLibrary.d.ts +31 -0
  73. package/dist/formulaLibrary.js +90 -0
  74. package/dist/index.d.ts +25 -24
  75. package/dist/index.js +15 -11
  76. package/dist/mediaManagerAdapter.d.ts +12 -0
  77. package/dist/mediaManagerAdapter.js +34 -0
  78. package/dist/mediaProvider.d.ts +21 -1
  79. package/dist/specialCharacters.d.ts +22 -0
  80. package/dist/specialCharacters.js +86 -0
  81. package/dist/standalone/classic-editor-embed.js +19 -12
  82. package/dist/suggestionProvider.d.ts +17 -0
  83. package/dist/suggestionProvider.js +1 -0
  84. package/dist/theme.d.ts +1 -1
  85. package/dist/theme.js +219 -30
  86. package/dist/toolbarTools.d.ts +95 -0
  87. package/dist/toolbarTools.js +17 -0
  88. package/dist/versionProvider.d.ts +29 -0
  89. package/dist/versionProvider.js +1 -0
  90. package/package.json +28 -14
  91. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  92. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  93. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  94. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  95. package/dist/adapters/coreBold.d.ts +0 -5
  96. package/dist/adapters/coreBold.js +0 -3
  97. package/dist/adapters/documentFormats.d.ts +0 -26
  98. package/dist/adapters/documentFormats.js +0 -49
  99. package/dist/adapters/docxFormat.d.ts +0 -5
  100. package/dist/adapters/docxFormat.js +0 -272
  101. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  102. package/dist/adapters/domBlockCommandBridge.js +0 -79
  103. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  104. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  105. package/dist/adapters/pdfFormat.d.ts +0 -23
  106. package/dist/adapters/pdfFormat.js +0 -204
  107. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  108. package/dist/adapters/portableDocxAtoms.js +0 -58
  109. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  110. package/dist/adapters/styledDocxFormat.js +0 -190
  111. package/dist/builtInFormatDefinitions.d.ts +0 -5
  112. package/dist/builtInFormatDefinitions.js +0 -82
  113. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  114. package/dist/canonicalAuthorityFlag.js +0 -49
  115. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  116. package/dist/canonicalClipboardRuntime.js +0 -76
  117. package/dist/components/ClassicEditor.d.ts +0 -76
  118. package/dist/components/ClassicEditor.js +0 -6729
  119. package/dist/editorController.d.ts +0 -78
  120. package/dist/editorController.js +0 -298
  121. package/dist/formatFidelity.d.ts +0 -14
  122. package/dist/formatFidelity.js +0 -107
  123. package/dist/formatRuntime.d.ts +0 -50
  124. package/dist/formatRuntime.js +0 -20
  125. package/dist/pluginRuntime.d.ts +0 -68
  126. package/dist/pluginRuntime.js +0 -78
  127. package/dist/standalone/editor.js +0 -1068
  128. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  129. package/dist/test-harness/atomShadowComparator.js +0 -63
  130. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  131. package/dist/test-harness/blockShadowComparator.js +0 -140
  132. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  133. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  134. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  135. package/dist/test-harness/inlineShadowComparator.js +0 -152
  136. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  137. package/dist/test-harness/legacyAtomEngine.js +0 -15
  138. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  139. package/dist/test-harness/legacyBlockEngine.js +0 -31
  140. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  141. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  142. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  143. package/dist/test-harness/legacyInlineEngine.js +0 -49
  144. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  145. package/dist/test-harness/legacyTableEngine.js +0 -25
  146. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  147. package/dist/test-harness/tableShadowComparator.js +0 -101
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.10
4
+
5
+ - Fix a table row resize regression: dragging a row boundary back to where it started (after an earlier drag hit a row's natural content floor) inflated the neighboring row instead of restoring the original layout — the only way back was Undo. Reversing a boundary drag now correctly shrinks the table's total height instead of growing a neighbor that never actually had slack to give.
6
+
7
+ - Fix a horizontal line/page break appearing to be "not selectable": clicking one already set real node selection, but right-clicking showed no UI at all, unlike every other atom (image/video/audio/formula). Right-click now shows the same Delete-only action menu formula already gets (never Edit — neither atom has an editable field, and neither ever gets resize handles).
8
+ - Add a "Line spacing" toolbar dropdown (Paragraph group) with presets (1, 1.15, 1.5, 2, 2.5), a "Default" entry that clears the override, and a custom numeric value entry, matching Google Docs' own line-spacing picker shape. Shows a checkmark on the current value, or no checkmark at all when the selection spans blocks with differing values. New `tools.lineHeight` visibility flag (default visible). Depends on `smartrte-core@1.0.0-beta.5`.
9
+ - Add "Horizontal line" and "Page break" toolbar tools (in the "More to insert" group, plus wide-viewport-promoted standalone copies), and matching `tools.horizontalLine`/`tools.pageBreak` visibility flags. A page break renders as a dashed, labeled marker in the live editor — distinct from a horizontal line — and produces real pagination in both the exported HTML and this package's "Save as PDF" (a real browser print of that same HTML). Depends on `smartrte-core@1.0.0-beta.5`.
10
+ - Fix formulas rendering invisible in "Save as PDF" output (see `smartrte-core@1.0.0-beta.5`'s own changelog entry for the root cause).
11
+ - Rename the "Save as PDF" toolbar entry to "Print / Save as PDF" — it opens the browser's print dialog rather than triggering a direct file download like its "Save as ..." siblings; the new label sets the correct expectation instead of implying a one-click download.
12
+
13
+ ## 1.0.0-beta.9
14
+
15
+ - Fix toolbar dropdowns, the mobile "More tools" kebab menu, and the text/background colour and table-border popovers rendering far from their own trigger when the editor is embedded inside a host container that has its own CSS `transform` on an ancestor - e.g. a Radix/shadcn `Dialog`'s own centering transform. These overlays now correctly account for the resulting change of containing block instead of assuming it's always the viewport.
16
+
17
+ ## 1.0.0-beta.8
18
+
19
+ - Add a `tools` prop (`ClassicEditor`, `CanonicalAuthorityEditor`, and the standalone embed) letting a host hide any individual toolbar tool — Bold, Video, Version history, Review, and around 40 others — without wrapping or restyling the component. Every tool is visible by default; composes with existing gates (a required provider or capability must still be present). See the README's "Hiding individual toolbar tools" section for the full list and usage.
20
+ - Retire the never-shipped `showVersionHistory`/`showReview` props in favor of `tools.versionHistory`/`tools.comments`/`tools.suggestions`.
21
+ - Also export `EditorCapabilityPreset` from the package's public entry point (previously only reachable indirectly through `CanonicalAuthorityEditorProps`).
22
+
23
+ ## 1.0.0-beta.7
24
+
25
+ - Fix the mobile "More tools" overflow menu getting clipped almost entirely off-screen when the editor is embedded inside a host container with `overflow: hidden` — the same fix already applied to the desktop toolbar dropdowns, now also covering this menu (the only overflow affordance on narrow viewports).
26
+ - Add the "List preset" control (decimal/lower-alpha/upper-alpha/roman-numeral/outline numbering styles, plus bullet-glyph presets) to the mobile "More tools" menu — previously only reachable from the desktop toolbar's "More list tools" dropdown, leaving narrow-viewport users with no way to use anything beyond plain bulleted/numbered/checklist lists.
27
+
28
+ ## 1.0.0-beta.6
29
+
30
+ - Replace the media Edit overlay's plain alt-text prompt with a full "Media details" panel: link (with open-in-new-tab), corner radius, alignment, and license fields (description, source URL, type, version, attribution), matching the pre-canonical editor's own edit-panel field set. A library-picked image's existing license metadata is now carried onto the inserted atom instead of being discarded. Depends on `smartrte-core@1.0.0-beta.3`.
31
+
32
+ ## 1.0.0-beta.5
33
+
34
+ - Add an opt-in `renderFormulaHtml` prop (on `ClassicEditor`/`CanonicalAuthorityEditor`) that bakes real KaTeX-rendered HTML into the formula elements of `onChange`'s exported HTML, instead of an empty placeholder — for hosts that display saved content directly (e.g. a read-only preview panel) without also running KaTeX against it. Depends on `smartrte-core@1.0.0-beta.2`.
35
+
36
+ ## 1.0.0-beta.4
37
+
38
+ - Fix blockquote, table header, and dialog (media library, comments, suggestions, version history) backgrounds/text staying in their light-mode colors even when the host page is in dark mode — these now follow the host's own dark-mode CSS variables directly, the same way the rest of the editor's colors already did, instead of depending solely on the `.srte-dark` class reaching the editor correctly.
39
+
40
+ ## 1.0.0-beta.3
41
+
42
+ - Fix toolbar dropdown menus (e.g. "More text styles", "More paragraph tools") getting visually clipped to unusable fragments when the editor is embedded inside a host container with `overflow: hidden` (e.g. a rounded card or a clipped split-pane panel) — the menu now positions itself relative to the viewport instead of the nearest ancestor, so it can never be clipped regardless of host layout.
43
+
44
+ ## 1.0.0-beta.2
45
+
46
+ - Fix the toolbar promoting extra buttons based on the browser window's width instead of the editor's own rendered width — broke embedding the editor in a split pane or any container narrower than the window, causing overlapping/mashed toolbar labels and misdirected clicks that looked like an unresponsive toolbar.
47
+
48
+ ## 1.0.0-beta.1
49
+
50
+ **BREAKING CHANGE.** `ClassicEditor` is now unconditionally the canonical, schema-driven editor (`CanonicalAuthorityEditor` underneath). The DOM-authoritative legacy editor and its rollback path have been fully removed; every published version through 0.3.4 defaulted to the DOM-authoritative implementation, so this changes real runtime behavior for every existing consumer, not just internals.
51
+
52
+ - Promote the canonical editing engine to sole production authority. Removed `LegacyClassicEditor` (the previous default), the runtime rollback flag, and all four of its DOM-authoritative command bridges.
53
+ - Add a documented, tested headless facade: `CanonicalEditorRuntime` / `SmartEditorHandle` (`createCanonicalEditorRuntime`) has zero React dependency and can drive a full editing session — mount, edit, checkpoint/restore, get/replace value — against a plain DOM element with no React runtime involved.
54
+ - Wire DOCX, PDF, and Markdown import/export to `smartrte-core`'s new format codecs, replacing the previous ad hoc, partially DOM-dependent adapters.
55
+ - Add live KaTeX rendering for formula content, including accessible MathML output.
56
+ - Depends on `smartrte-core@1.0.0-beta.1`.
57
+
3
58
  ## 0.3.5
4
59
 
5
60
  - Remove the always-on drag handle that appeared next to every line, list item, and table.
package/README.md CHANGED
@@ -41,7 +41,7 @@ pnpm add smartrte-react
41
41
  ## 🚀 Quick Start
42
42
 
43
43
  ### 🎮 Live Demo
44
-
44
+
45
45
  Try the editor instantly in your browser:
46
46
 
47
47
  - **[Live Demo](https://playground-k9l44ah7t-ayush1852017s-projects.vercel.app/)** (Deployed Version)
@@ -87,10 +87,136 @@ export default App;
87
87
  | `table` | `boolean` | `true` | Enable/disable table functionality |
88
88
  | `media` | `boolean` | `true` | Enable/disable media/image functionality |
89
89
  | `formula` | `boolean` | `true` | Enable/disable formula/LaTeX functionality |
90
+ | `features` | `CoreFeatureConfig` | standard preset | Enable or disable individual core plugins |
91
+ | `plugins` | `ReactEditorPlugin[]` | standard preset | Use an exact custom plugin set; replaces the standard preset |
92
+ | `formats` | `EditorFormatConfig` | all built-ins | Enable or disable HTML, Markdown, DOCX, and PDF adapters |
93
+ | `formatDefinitions` | `EditorFormatDefinition[]` | built-ins | Add or replace import/export adapters |
90
94
  | `mediaManager` | `MediaManagerAdapter` | `undefined` | Custom media manager for handling images |
95
+ | `fonts` | `{ name: string; value: string }[]` | Default web-safe fonts | Custom font list for the toolbar |
96
+ | `defaultFont` | `string` | `undefined` | Default font family for the editor content |
97
+ | `theme` | `"light" \| "dark"` | `"light"` | Built-in theme mode |
98
+ | `className` | `string` | `undefined` | Custom CSS class for theming via CSS variable overrides |
99
+ | `tools` | `Partial<ToolbarTools>` | every tool `true` | Hide individual toolbar tools (Bold, Video, Version history, etc.) — see [Hiding individual toolbar tools](#hiding-individual-toolbar-tools) below |
91
100
 
92
101
  ### Advanced Examples
93
102
 
103
+ #### Feature plugins
104
+
105
+ The editor uses the same plugin registry for core commands and React toolbar
106
+ state. For the architecture and custom plugin API, see
107
+ [`docs/PLUGIN_ARCHITECTURE.md`](../../docs/PLUGIN_ARCHITECTURE.md).
108
+
109
+ ```tsx
110
+ import { ClassicEditor } from 'smartrte-react';
111
+
112
+ <ClassicEditor
113
+ features={{
114
+ table: false,
115
+ media: false,
116
+ formula: false,
117
+ checklist: true,
118
+ }}
119
+ />
120
+ ```
121
+
122
+ #### Hiding individual toolbar tools
123
+
124
+ The `table`/`media`/`formula` props above turn off a whole *capability* — no
125
+ tables anywhere, or media/formulas removed from the schema entirely. If you
126
+ just want a **smaller toolbar** while keeping every capability intact (e.g.
127
+ hide Video, Audio, Version history, and Review for a specific product
128
+ surface, without touching what content the editor can actually store), use
129
+ `tools` instead. It's a plain object: name the tools you want off, everything
130
+ else stays on by default. No wrapping, no CSS overrides, no forking the
131
+ component.
132
+
133
+ ```tsx
134
+ import { ClassicEditor } from 'smartrte-react';
135
+
136
+ <ClassicEditor
137
+ tools={{
138
+ video: false,
139
+ audio: false,
140
+ versionHistory: false,
141
+ comments: false,
142
+ suggestions: false,
143
+ }}
144
+ />
145
+ ```
146
+
147
+ **How it composes with everything else:** a tool only ever needs *both*
148
+ things to be true to show up — your `tools` flag says yes, *and* whatever
149
+ that tool actually depends on is present. So:
150
+
151
+ - `tools.video`/`tools.audio`/`tools.image` also need a `mediaProvider` (or
152
+ `mediaManager`) to be configured at all — turning the flag on can't make
153
+ media insertion appear out of nowhere if you never wired up a provider.
154
+ - `tools.versionHistory` also needs a `versionProvider`; `tools.comments`
155
+ needs a `commentProvider`; `tools.suggestions` needs a `suggestionProvider`.
156
+ - `tools.insertTable` also needs the table capability to actually be enabled
157
+ (i.e. you haven't set `table={false}`) — you can't use `tools` to bring
158
+ back a capability you turned off elsewhere.
159
+
160
+ In other words: `tools` can only ever hide something, never force something
161
+ into existence that isn't otherwise configured. This means it's always safe
162
+ to leave `tools` unset — every consumer who doesn't pass it sees the exact
163
+ toolbar they see today.
164
+
165
+ **Every toggleable key**, grouped the way they appear in the toolbar:
166
+
167
+ ```ts
168
+ interface ToolbarTools {
169
+ // Text formatting
170
+ bold, italic, underline, strikethrough, code,
171
+ superscript, subscript, textColor, backgroundColor, fontSize, fontFamily,
172
+
173
+ // Paragraph
174
+ blockType, // the Paragraph/Heading/Code block dropdown
175
+ alignLeft, alignCenter, alignRight, alignJustify,
176
+ lineHeight, // the line-spacing dropdown (1/1.15/1.5/2/2.5 presets plus a custom value)
177
+ quote,
178
+
179
+ // Lists
180
+ bulletedList, numberedList, checklist,
181
+ listPreset, // the numbered-list style picker (1,2,3 / a,b,c / i,ii,iii / A,B,C / I,II,III)
182
+
183
+ // Insert
184
+ link, removeLink, image, video, audio, insertFormula, specialCharacters,
185
+ horizontalLine, // inserts a divider (<hr>)
186
+ pageBreak, // inserts a print/export pagination marker - distinct from horizontalLine
187
+ insertTable,
188
+
189
+ // Document
190
+ import,
191
+ saveAsHtml, saveAsMarkdown, saveAsWord, saveAsPdf, saveAsSmartRte,
192
+ versionHistory, comments, suggestions,
193
+
194
+ // History
195
+ undo, redo,
196
+ }
197
+ ```
198
+
199
+ A few notes on what's *not* in this list, on purpose: actions that only ever
200
+ apply to something you already have selected — moving a block up/down,
201
+ indenting a list item, adding/removing a table row or column, resizing a
202
+ selected image — aren't individually toggleable. They only make sense as
203
+ part of using the tool that created them (a table, a list, an image), so
204
+ they follow that tool's own visibility rather than needing a separate flag
205
+ each. If you turn off `insertTable`, its row/column tools go with it
206
+ automatically — there's no separate flag to remember.
207
+
208
+ If you're using the exported `CanonicalAuthorityEditor` directly instead of
209
+ `ClassicEditor`, `tools` works exactly the same way. And if you're using the
210
+ standalone embed (`window.SmartRTE.ClassicEditor.init(...)`), pass `tools`
211
+ in the same options object you pass `target`/`value`/etc.
212
+
213
+ ```ts
214
+ window.SmartRTE.ClassicEditor.init({
215
+ target: document.getElementById('editor'),
216
+ tools: { video: false, audio: false },
217
+ });
218
+ ```
219
+
94
220
  #### Complete Example with All Features
95
221
 
96
222
  ```tsx
@@ -314,23 +440,87 @@ const myMediaManager: MediaManagerAdapter = {
314
440
  };
315
441
  ```
316
442
 
317
- ## 🎨 Styling
443
+ ## 🎨 Theming & Dark Mode
318
444
 
319
- The editor comes with built-in styles. You can customize the appearance by wrapping it in a container:
445
+ The editor uses CSS custom properties (CSS variables) for all colors, making it fully customizable. No hardcoded colors — everything can be themed.
446
+
447
+ ### Quick Start: Dark Mode
448
+
449
+ ```tsx
450
+ <ClassicEditor theme="dark" onChange={handleChange} />
451
+ ```
452
+
453
+ ### Custom Themes via CSS
454
+
455
+ Apply a custom class and override any CSS variables:
320
456
 
321
457
  ```tsx
322
- <div style={{
323
- border: '1px solid #ddd',
324
- borderRadius: '8px',
325
- overflow: 'hidden',
326
- }}>
327
- <ClassicEditor
328
- value={content}
329
- onChange={setContent}
330
- />
331
- </div>
458
+ <ClassicEditor className="my-theme" onChange={handleChange} />
332
459
  ```
333
460
 
461
+ ```css
462
+ .my-theme {
463
+ --srte-bg: #1a1a2e;
464
+ --srte-text: #eaeaea;
465
+ --srte-border: #3a3a5c;
466
+ /* Override only the variables you need */
467
+ }
468
+ ```
469
+
470
+ ### Responding to System Preference
471
+
472
+ ```tsx
473
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
474
+
475
+ <ClassicEditor theme={prefersDark ? 'dark' : 'light'} />
476
+ ```
477
+
478
+ Or purely via CSS (without the `theme` prop):
479
+
480
+ ```css
481
+ @media (prefers-color-scheme: dark) {
482
+ .srte-editor {
483
+ --srte-bg: #1e1e1e;
484
+ --srte-text: #e0e0e0;
485
+ --srte-border: #3a3a3a;
486
+ /* ... */
487
+ }
488
+ }
489
+ ```
490
+
491
+ ### Available CSS Custom Properties
492
+
493
+ | Variable | Default (Light) | Description |
494
+ |---|---|---|
495
+ | `--srte-bg` | `#ffffff` | Editor container & content background |
496
+ | `--srte-text` | `#111111` | Primary UI text color |
497
+ | `--srte-text-muted` | `#4b5563` | Secondary/subtle text |
498
+ | `--srte-border` | `#dddddd` | Standard border color |
499
+ | `--srte-border-light` | `#eeeeee` | Light separator borders |
500
+ | `--srte-toolbar-bg` | `#ffffff` | Toolbar background |
501
+ | `--srte-input-bg` | `#ffffff` | Button, input, select backgrounds |
502
+ | `--srte-input-text` | `#111111` | Button, input, select text |
503
+ | `--srte-input-border` | `#e5e7eb` | Button, input, select borders |
504
+ | `--srte-modal-backdrop` | `rgba(0,0,0,0.35)` | Modal overlay background |
505
+ | `--srte-modal-bg` | `#ffffff` | Modal/dialog background |
506
+ | `--srte-modal-text` | `#000000` | Modal text color |
507
+ | `--srte-menu-bg` | `#ffffff` | Context menu background |
508
+ | `--srte-menu-text` | `#111111` | Context menu text |
509
+ | `--srte-menu-shadow` | `0 8px 24px rgba(0,0,0,0.18)` | Menu box-shadow |
510
+ | `--srte-accent` | `#1e90ff` | Accent/selection color |
511
+ | `--srte-accent-bg` | `rgba(30,144,255,0.15)` | Accent background (translucent) |
512
+ | `--srte-danger` | `#dc2626` | Destructive action color |
513
+ | `--srte-primary` | `#2563eb` | Primary action button color |
514
+ | `--srte-surface-subtle` | `#f3f4f6` | Subtle surface (dropzone, preset buttons) |
515
+ | `--srte-on-primary` | `#ffffff` | Text on primary/danger buttons |
516
+ | `--srte-cancel-bg` | `#f3f4f6` | Cancel button background |
517
+
518
+ ### Important Notes
519
+
520
+ - **User content colors are preserved** — colors set via the color picker (text/background) are inline styles on content elements and are not affected by theming.
521
+ - **Color picker swatches are not themed** — they display actual color values regardless of theme.
522
+ - **The theme only affects editor chrome** — toolbar, dialogs, menus, and container. User content remains unchanged.
523
+
334
524
  ## 🛠️ Development
335
525
 
336
526
  ### Prerequisites
@@ -626,13 +816,14 @@ See the list of [contributors](https://github.com/ayush1852017/smart-rte/contrib
626
816
 
627
817
  ## 🗺️ Roadmap
628
818
 
629
- ### Current Version (0.1.x)
819
+ ### Current Version (0.2.x)
630
820
 
631
821
  - ✅ Rich text editing
632
822
  - ✅ Table support
633
823
  - ✅ Formula support (LaTeX/KaTeX)
634
824
  - ✅ Media management
635
825
  - ✅ TypeScript support
826
+ - ✅ Dark mode & theming (CSS custom properties)
636
827
 
637
828
  ### Upcoming Features
638
829
 
@@ -1,4 +1,4 @@
1
- import { checklistPlugin, createSmartEditor, listPlugin } from "smartrte-core";
1
+ import { checklistPlugin, createSmartEditor, listPlugin } from "smartrte-core/legacy";
2
2
  import { smartDocumentFromHtml } from "./domSmartDocument.js";
3
3
  /**
4
4
  * Validates and executes a checklist mutation through core, then projects the
@@ -1,7 +1,7 @@
1
- import { type SmartRtePlugin, type SmartSelection } from "smartrte-core";
1
+ import { type SmartRtePlugin, type LegacySmartSelection } from "smartrte-core/legacy";
2
2
  export interface DomCommandResult {
3
3
  html: string;
4
- selection: SmartSelection;
4
+ selection: LegacySmartSelection;
5
5
  }
6
6
  /**
7
7
  * Executes one model command against the live editor boundary.
@@ -1,4 +1,4 @@
1
- import { createSmartEditor, } from "smartrte-core";
1
+ import { createSmartEditor, } from "smartrte-core/legacy";
2
2
  import { restoreSelectionToDom, selectionFromDom } from "./domSelectionBridge.js";
3
3
  import { serializeSmartDocument, smartDocumentFromEditorRoot, } from "./domSmartDocument.js";
4
4
  /**
@@ -9,7 +9,7 @@ import { serializeSmartDocument, smartDocumentFromEditorRoot, } from "./domSmart
9
9
  * the React host until the complete editor runtime is migrated.
10
10
  */
11
11
  export const executeDomCommand = (args) => {
12
- // These nodes are not represented by SmartDocument yet. Falling back is
12
+ // These nodes are not represented by LegacySmartDocument yet. Falling back is
13
13
  // mandatory so a supported command can never erase unsupported content.
14
14
  if (args.root.querySelector("img,video,audio,iframe")) {
15
15
  return null;
@@ -1,14 +1,15 @@
1
- import { type Path } from "smartrte-core";
2
- export declare const inlineAtomPathFromDom: (root: HTMLElement, atom: HTMLElement) => Path | null;
3
- /**
4
- * Runs deletion through the owning feature plugin and removes only the
5
- * validated live atom. Caret placement remains a DOM-host responsibility.
6
- */
7
- export declare const executeDomInlineAtomDelete: (root: HTMLElement, atom: HTMLElement) => boolean;
1
+ import { type Attrs } from "smartrte-core/foundation";
2
+ export declare const inlineAtomPathFromDom: (root: HTMLElement, atom: HTMLElement) => number[] | null;
8
3
  export interface DomFormulaInput {
9
4
  value: string;
10
5
  displayText?: string;
11
6
  }
12
- /** Validates formula insertion through core, then projects only the new atom. */
13
- export declare const executeDomFormulaInsert: (root: HTMLElement, input: DomFormulaInput, domSelection?: Selection | null) => HTMLElement | null;
7
+ export interface DomCanonicalAtomInput {
8
+ type: "image" | "formula";
9
+ attrs: Attrs;
10
+ }
11
+ export declare const executeDomCanonicalAtomInsert: (root: HTMLElement, input: DomCanonicalAtomInput, domSelection?: Selection | null) => HTMLElement | null;
12
+ export declare const executeDomFormulaInsert: (root: HTMLElement, input: DomFormulaInput, selection?: Selection | null) => HTMLElement | null;
13
+ export declare const executeDomInlineAtomDelete: (root: HTMLElement, atom: HTMLElement) => boolean;
14
+ export declare const executeDomCanonicalAtomUpdate: (root: HTMLElement, atom: HTMLElement, attrs: Attrs) => boolean;
14
15
  export declare const adjacentInlineAtom: (range: Range, direction: "backward" | "forward") => HTMLElement | null;
@@ -1,10 +1,10 @@
1
- import { createSmartEditor, formulaPlugin, getNodeAtPath, mediaPlugin, } from "smartrte-core";
2
- import { isEditorOnlyElement, pathForDomElement, selectionFromDom } from "./domSelectionBridge.js";
3
- import { smartDocumentFromEditorRoot } from "./domSmartDocument.js";
1
+ import { applyOperations, atomDeclarations, createNodeId, createScopeIndex, deleteAtom, foundationSchema, insertAtom, parseCanonicalListHtml, renderDocumentNaively, updateAtom, } from "smartrte-core/foundation";
2
+ import { isEditorOnlyElement, pathForDomElement } from "./domSelectionBridge.js";
4
3
  const atomType = (element) => {
5
- if (element.tagName.toLowerCase() === "img")
6
- return "inlineImage";
7
- if (element.hasAttribute("data-formula"))
4
+ const declared = element.getAttribute("data-smart-type");
5
+ if (declared === "image" || element.tagName.toLowerCase() === "img")
6
+ return "image";
7
+ if (declared === "formula" || element.hasAttribute("data-formula") || element.hasAttribute("data-smart-formula"))
8
8
  return "formula";
9
9
  return null;
10
10
  };
@@ -35,61 +35,165 @@ export const inlineAtomPathFromDom = (root, atom) => {
35
35
  const index = inlineUnits(leaf).indexOf(atom);
36
36
  return leafPath && index >= 0 ? [...leafPath, index] : null;
37
37
  };
38
- /**
39
- * Runs deletion through the owning feature plugin and removes only the
40
- * validated live atom. Caret placement remains a DOM-host responsibility.
41
- */
42
- export const executeDomInlineAtomDelete = (root, atom) => {
43
- const type = atomType(atom);
44
- const path = type && inlineAtomPathFromDom(root, atom);
45
- if (!type || !path)
46
- return false;
47
- const selection = selectionFromDom(root, root.ownerDocument.defaultView?.getSelection() || null) || { type: "node", path };
48
- const { document } = smartDocumentFromEditorRoot(root);
49
- const editor = createSmartEditor({
50
- state: { document, selection },
51
- plugins: type === "formula" ? [formulaPlugin] : [mediaPlugin],
38
+ const ownerSelector = "p,h1,h2,h3,h4,h5,h6";
39
+ const prepareOwner = (owner) => {
40
+ var _a;
41
+ (_a = owner.dataset).smartId || (_a.smartId = createNodeId());
42
+ owner.querySelectorAll("img,[data-formula],[data-smart-formula]").forEach((atom) => {
43
+ var _a, _b, _c;
44
+ (_a = atom.dataset).smartId || (_a.smartId = createNodeId());
45
+ if (atomType(atom) === "formula") {
46
+ atom.dataset.smartType = "formula";
47
+ atom.dataset.smartFormula = atom.dataset.smartFormula || atom.dataset.formula || atom.textContent || "";
48
+ (_b = atom.dataset).smartNotation || (_b.smartNotation = "latex");
49
+ atom.dataset.smartAtomic = "true";
50
+ }
51
+ else {
52
+ atom.dataset.smartType = "image";
53
+ (_c = atom.dataset).smartStatus || (_c.smartStatus = "ready");
54
+ atom.dataset.smartAtomic = "true";
55
+ }
52
56
  });
53
- const command = type === "formula" ? "formula.delete" : "image.delete-inline";
54
- if (!editor.execute(command, { path }))
57
+ };
58
+ const parseOwner = (owner) => {
59
+ prepareOwner(owner);
60
+ return parseCanonicalListHtml(owner.outerHTML);
61
+ };
62
+ const contentWidth = (node) => {
63
+ if (node.nodeType === node.TEXT_NODE)
64
+ return node.nodeValue?.length || 0;
65
+ if (node instanceof HTMLElement && atomType(node))
66
+ return 1;
67
+ if (node instanceof HTMLElement && isEditorOnlyElement(node))
68
+ return 0;
69
+ return [...node.childNodes].reduce((sum, child) => sum + contentWidth(child), 0);
70
+ };
71
+ const caretOffset = (owner, range) => {
72
+ const before = owner.ownerDocument.createRange();
73
+ before.selectNodeContents(owner);
74
+ try {
75
+ before.setEnd(range.startContainer, range.startOffset);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ return contentWidth(before.cloneContents());
81
+ };
82
+ const renderOwner = (owner, document) => {
83
+ const projection = owner.ownerDocument.createElement("div");
84
+ renderDocumentNaively(projection, document);
85
+ const replacement = Array.from(projection.querySelectorAll("[data-smart-id]"))
86
+ .find((element) => element.dataset.smartId === owner.dataset.smartId);
87
+ if (!replacement)
55
88
  return false;
56
- atom.remove();
89
+ owner.replaceChildren(...Array.from(replacement.childNodes));
57
90
  return true;
58
91
  };
59
- /** Validates formula insertion through core, then projects only the new atom. */
60
- export const executeDomFormulaInsert = (root, input, domSelection = root.ownerDocument.defaultView?.getSelection() || null) => {
92
+ export const executeDomCanonicalAtomInsert = (root, input, domSelection = root.ownerDocument.defaultView?.getSelection() || null) => {
61
93
  if (!domSelection?.rangeCount || !domSelection.isCollapsed)
62
94
  return null;
63
- const modelSelection = selectionFromDom(root, domSelection);
64
- if (!modelSelection)
95
+ const range = domSelection.getRangeAt(0);
96
+ const element = range.startContainer instanceof Element ? range.startContainer : range.startContainer.parentElement;
97
+ const owner = element?.closest(ownerSelector);
98
+ if (!owner || !root.contains(owner))
65
99
  return null;
66
- const { document } = smartDocumentFromEditorRoot(root);
67
- const editor = createSmartEditor({
68
- state: { document, selection: modelSelection },
69
- plugins: [formulaPlugin],
70
- });
71
- if (!editor.execute("formula.insert", input))
100
+ const offset = caretOffset(owner, range);
101
+ if (offset === null)
72
102
  return null;
73
- const selectionAfter = editor.state.selection;
74
- if (selectionAfter.type !== "text")
103
+ const document = parseOwner(owner);
104
+ const declaration = atomDeclarations.find((entry) => entry.type === input.type);
105
+ const nodeId = createNodeId();
106
+ const index = createScopeIndex();
107
+ const operations = insertAtom(document, { kind: "empty", range: { from: { path: [0], offset }, to: { path: [0], offset } }, isolatingAncestorId: null, clamped: false }, {
108
+ declaration, nodeId, attrs: input.attrs, ownerId: owner.dataset.smartId, offset,
109
+ }, { schema: foundationSchema, positions: index.positions(document, foundationSchema) });
110
+ if (!operations.length || !renderOwner(owner, applyOperations(document, operations)))
75
111
  return null;
76
- const formulaPath = [
77
- ...selectionAfter.anchor.path.slice(0, -1),
78
- selectionAfter.anchor.path[selectionAfter.anchor.path.length - 1] - 1,
79
- ];
80
- const atom = getNodeAtPath(editor.state.document, formulaPath);
81
- if (atom?.type !== "formula")
112
+ const atom = Array.from(owner.querySelectorAll("[data-smart-id]"))
113
+ .find((element) => element.dataset.smartId === nodeId) || null;
114
+ if (!atom)
82
115
  return null;
83
- const range = domSelection.getRangeAt(0);
84
- const span = root.ownerDocument.createElement("span");
85
- span.dataset.formula = atom.value;
86
- span.textContent = atom.displayText ?? atom.value;
87
- range.insertNode(span);
88
- range.setStartAfter(span);
89
- range.collapse(true);
116
+ if (input.type === "formula") {
117
+ atom.dataset.formula = String(input.attrs.source || "");
118
+ atom.textContent = String(input.attrs.displayText || input.attrs.source || "");
119
+ }
120
+ else
121
+ atom.dataset.srteInline = "true";
122
+ const next = root.ownerDocument.createRange();
123
+ next.setStartAfter(atom);
124
+ next.collapse(true);
90
125
  domSelection.removeAllRanges();
91
- domSelection.addRange(range);
92
- return span;
126
+ domSelection.addRange(next);
127
+ return atom;
128
+ };
129
+ export const executeDomFormulaInsert = (root, input, selection) => executeDomCanonicalAtomInsert(root, { type: "formula", attrs: { source: input.value.trim(), notation: "latex", ...(input.displayText ? { displayText: input.displayText } : {}) } }, selection);
130
+ export const executeDomInlineAtomDelete = (root, atom) => {
131
+ const owner = atom.closest(ownerSelector);
132
+ if (!owner || !root.contains(owner) || !atomType(atom))
133
+ return false;
134
+ prepareOwner(owner);
135
+ const id = atom.dataset.smartId;
136
+ const document = parseOwner(owner);
137
+ const index = createScopeIndex();
138
+ const positions = index.positions(document, foundationSchema);
139
+ const range = positions.rangeOf(id);
140
+ if (!range)
141
+ return false;
142
+ const scope = { kind: "atomic-node", nodeId: id, inline: true, range, isolatingAncestorId: null, clamped: false };
143
+ const operations = deleteAtom(document, scope, {}, { schema: foundationSchema, positions });
144
+ return operations.length > 0 && renderOwner(owner, applyOperations(document, operations));
145
+ };
146
+ export const executeDomCanonicalAtomUpdate = (root, atom, attrs) => {
147
+ const owner = atom.closest(ownerSelector);
148
+ if (!owner || !root.contains(owner) || !atomType(atom))
149
+ return false;
150
+ prepareOwner(owner);
151
+ const id = atom.dataset.smartId;
152
+ const document = parseOwner(owner);
153
+ const index = createScopeIndex();
154
+ const positions = index.positions(document, foundationSchema);
155
+ const range = positions.rangeOf(id);
156
+ if (!range)
157
+ return false;
158
+ const scope = { kind: "atomic-node", nodeId: id, inline: true, range, isolatingAncestorId: null, clamped: false };
159
+ const operations = updateAtom(document, scope, { attrs }, { schema: foundationSchema, positions });
160
+ if (!operations.length)
161
+ return false;
162
+ const output = applyOperations(document, operations);
163
+ const nextPositions = createScopeIndex().positions(output, foundationSchema);
164
+ const resolved = nextPositions.positionOf(id);
165
+ const next = resolved?.parent.children?.find((child) => child.type !== "text" && "id" in child && child.id === id);
166
+ if (!next || next.type === "text")
167
+ return false;
168
+ if (atom instanceof HTMLImageElement) {
169
+ if (typeof next.attrs?.src === "string")
170
+ atom.src = next.attrs.src;
171
+ if (typeof next.attrs?.alt === "string")
172
+ atom.alt = next.attrs.alt;
173
+ if (typeof next.attrs?.title === "string")
174
+ atom.title = next.attrs.title;
175
+ if (Number(next.attrs?.width) > 0)
176
+ atom.width = Number(next.attrs?.width);
177
+ if (Number(next.attrs?.height) > 0)
178
+ atom.height = Number(next.attrs?.height);
179
+ const align = next.attrs?.align;
180
+ if (align === "center") {
181
+ atom.style.display = "block";
182
+ atom.style.margin = "0 auto";
183
+ atom.style.float = "none";
184
+ }
185
+ else if (align === "left" || align === "right") {
186
+ atom.style.display = "inline";
187
+ atom.style.float = align;
188
+ atom.style.margin = align === "left" ? "0 8px 8px 0" : "0 0 8px 8px";
189
+ }
190
+ atom.dataset.smartStatus = String(next.attrs?.status || "ready");
191
+ if (typeof next.attrs?.error === "string")
192
+ atom.dataset.smartError = next.attrs.error;
193
+ else
194
+ delete atom.dataset.smartError;
195
+ }
196
+ return true;
93
197
  };
94
198
  export const adjacentInlineAtom = (range, direction) => {
95
199
  if (!range.collapsed || range.startContainer.nodeType !== Node.ELEMENT_NODE)
@@ -1,14 +1,14 @@
1
1
  export interface DomInlineImageInput {
2
2
  src: string;
3
3
  alt?: string;
4
+ decorative?: boolean;
4
5
  title?: string;
5
6
  width?: number;
6
7
  height?: number;
8
+ align?: "center" | "left" | "right";
9
+ status?: "pending" | "ready" | "error";
10
+ uploadId?: string;
11
+ error?: string;
7
12
  }
8
- /**
9
- * Executes inline image insertion through core and projects only the resulting
10
- * atom into the existing DOM range. Unrelated DOM and metadata stay untouched.
11
- */
12
- export declare const executeDomInlineImageCommand: (root: HTMLElement, input: DomInlineImageInput, domSelection?: Selection | null) => HTMLImageElement | null;
13
- /** Validates an inline image attribute update through core, then mutates it in place. */
14
- export declare const executeDomInlineImageUpdate: (root: HTMLElement, image: HTMLImageElement, input: Omit<DomInlineImageInput, "src">) => boolean;
13
+ export declare const executeDomInlineImageCommand: (root: HTMLElement, input: DomInlineImageInput, selection?: Selection | null) => HTMLImageElement | null;
14
+ export declare const executeDomInlineImageUpdate: (root: HTMLElement, image: HTMLImageElement, input: Partial<DomInlineImageInput>) => boolean;