smartrte-react 0.3.5 → 1.0.0-beta.2

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 (141) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +106 -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 +53 -1
  25. package/dist/canonicalEditorRuntime.js +61 -4
  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 +35 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1086 -112
  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 +285 -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/MediaManager.d.ts +4 -20
  49. package/dist/components/MediaManager.js +35 -6
  50. package/dist/components/MediaOverlay.d.ts +36 -0
  51. package/dist/components/MediaOverlay.js +163 -0
  52. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  53. package/dist/components/SpecialCharacterPopover.js +99 -0
  54. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  55. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  56. package/dist/components/SuggestionPanel.d.ts +26 -0
  57. package/dist/components/SuggestionPanel.js +33 -0
  58. package/dist/components/TableBorderPopover.d.ts +59 -0
  59. package/dist/components/TableBorderPopover.js +147 -0
  60. package/dist/components/TableResizeHandles.d.ts +25 -0
  61. package/dist/components/TableResizeHandles.js +301 -0
  62. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  63. package/dist/components/TableSizePickerPopover.js +126 -0
  64. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  65. package/dist/components/ToolbarPrimitives.js +146 -0
  66. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  67. package/dist/components/VersionHistoryPanel.js +157 -0
  68. package/dist/formulaLibrary.d.ts +31 -0
  69. package/dist/formulaLibrary.js +90 -0
  70. package/dist/index.d.ts +22 -24
  71. package/dist/index.js +14 -11
  72. package/dist/mediaManagerAdapter.d.ts +12 -0
  73. package/dist/mediaManagerAdapter.js +34 -0
  74. package/dist/mediaProvider.d.ts +21 -1
  75. package/dist/specialCharacters.d.ts +22 -0
  76. package/dist/specialCharacters.js +86 -0
  77. package/dist/standalone/classic-editor-embed.js +19 -12
  78. package/dist/suggestionProvider.d.ts +17 -0
  79. package/dist/suggestionProvider.js +1 -0
  80. package/dist/theme.d.ts +1 -1
  81. package/dist/theme.js +150 -20
  82. package/dist/versionProvider.d.ts +29 -0
  83. package/dist/versionProvider.js +1 -0
  84. package/package.json +28 -14
  85. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  86. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  87. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  88. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  89. package/dist/adapters/coreBold.d.ts +0 -5
  90. package/dist/adapters/coreBold.js +0 -3
  91. package/dist/adapters/documentFormats.d.ts +0 -26
  92. package/dist/adapters/documentFormats.js +0 -49
  93. package/dist/adapters/docxFormat.d.ts +0 -5
  94. package/dist/adapters/docxFormat.js +0 -272
  95. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  96. package/dist/adapters/domBlockCommandBridge.js +0 -79
  97. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  98. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  99. package/dist/adapters/pdfFormat.d.ts +0 -23
  100. package/dist/adapters/pdfFormat.js +0 -204
  101. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  102. package/dist/adapters/portableDocxAtoms.js +0 -58
  103. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  104. package/dist/adapters/styledDocxFormat.js +0 -190
  105. package/dist/builtInFormatDefinitions.d.ts +0 -5
  106. package/dist/builtInFormatDefinitions.js +0 -82
  107. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  108. package/dist/canonicalAuthorityFlag.js +0 -49
  109. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  110. package/dist/canonicalClipboardRuntime.js +0 -76
  111. package/dist/components/ClassicEditor.d.ts +0 -76
  112. package/dist/components/ClassicEditor.js +0 -6729
  113. package/dist/editorController.d.ts +0 -78
  114. package/dist/editorController.js +0 -298
  115. package/dist/formatFidelity.d.ts +0 -14
  116. package/dist/formatFidelity.js +0 -107
  117. package/dist/formatRuntime.d.ts +0 -50
  118. package/dist/formatRuntime.js +0 -20
  119. package/dist/pluginRuntime.d.ts +0 -68
  120. package/dist/pluginRuntime.js +0 -78
  121. package/dist/standalone/editor.js +0 -1068
  122. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  123. package/dist/test-harness/atomShadowComparator.js +0 -63
  124. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  125. package/dist/test-harness/blockShadowComparator.js +0 -140
  126. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  127. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  128. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  129. package/dist/test-harness/inlineShadowComparator.js +0 -152
  130. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  131. package/dist/test-harness/legacyAtomEngine.js +0 -15
  132. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  133. package/dist/test-harness/legacyBlockEngine.js +0 -31
  134. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  135. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  136. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  137. package/dist/test-harness/legacyInlineEngine.js +0 -49
  138. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  139. package/dist/test-harness/legacyTableEngine.js +0 -25
  140. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  141. package/dist/test-harness/tableShadowComparator.js +0 -101
package/dist/theme.js CHANGED
@@ -27,6 +27,11 @@ export const SRTE_DEFAULT_CSS = `
27
27
  --srte-accent-bg: rgba(2, 132, 199, 0.12);
28
28
  --srte-danger: #dc2626;
29
29
  --srte-primary: #2563eb;
30
+ /* --srte-primary on --srte-accent-bg measures 4.43:1, under WCAG AA's
31
+ 4.5:1 for normal text - used only for the pressed toolbar-button state
32
+ (see .srte-tool-button[aria-pressed="true"]), not a replacement for
33
+ --srte-primary generally. */
34
+ --srte-primary-pressed: #1d4ed8;
30
35
  --srte-surface-subtle: #f3f4f6;
31
36
  --srte-on-primary: #ffffff;
32
37
  --srte-cancel-bg: #f3f4f6;
@@ -60,6 +65,7 @@ export const SRTE_DEFAULT_CSS = `
60
65
  --srte-accent-bg: rgba(56, 189, 248, 0.16);
61
66
  --srte-danger: #ef4444;
62
67
  --srte-primary: #3b82f6;
68
+ --srte-primary-pressed: var(--srte-primary);
63
69
  --srte-surface-subtle: #333333;
64
70
  --srte-on-primary: #ffffff;
65
71
  --srte-cancel-bg: #333333;
@@ -71,6 +77,18 @@ export const SRTE_DEFAULT_CSS = `
71
77
  font-family: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
72
78
  container: srte-editor / inline-size;
73
79
  }
80
+ .srte-canonical-authority > .srte-editor[contenteditable] {
81
+ width: 100%;
82
+ padding: 16px 20px;
83
+ box-sizing: border-box;
84
+ border: 1px solid var(--srte-border);
85
+ border-radius: 0 0 var(--srte-radius) var(--srte-radius);
86
+ outline: none;
87
+ background: var(--srte-canvas);
88
+ color: var(--srte-foreground);
89
+ caret-color: var(--srte-foreground);
90
+ line-height: 1.6;
91
+ }
74
92
  .srte-toolbar {
75
93
  display: flex;
76
94
  flex-wrap: wrap;
@@ -134,7 +152,12 @@ export const SRTE_DEFAULT_CSS = `
134
152
  }
135
153
  .srte-tool-button.srte-active,
136
154
  .srte-tool-button[aria-pressed="true"] {
137
- color: var(--srte-primary);
155
+ /* var(--srte-primary) (#2563eb) on var(--srte-accent-bg) measured 4.43:1,
156
+ just under WCAG AA's 4.5:1 for normal text (axe-core, Phase 11 Tier 3) -
157
+ a dedicated, darker pressed-state color keeps the same hue family
158
+ while clearing the threshold, without changing --srte-primary's other,
159
+ already-compliant usages (e.g. against solid backgrounds). */
160
+ color: var(--srte-primary-pressed);
138
161
  background: var(--srte-accent-bg);
139
162
  border-color: color-mix(in srgb, var(--srte-primary) 35%, transparent);
140
163
  }
@@ -241,6 +264,56 @@ export const SRTE_DEFAULT_CSS = `
241
264
  .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-decimal-paren"][data-srte-list-depth="3"] > li::marker {
242
265
  content: counter(list-item, lower-roman) ") ";
243
266
  }
267
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-decimal"][data-srte-list-depth="0"] > li::marker {
268
+ content: counter(list-item, decimal) ". ";
269
+ }
270
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-decimal"][data-srte-list-depth="1"] > li::marker {
271
+ content: counter(list-item, lower-alpha) ". ";
272
+ }
273
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-decimal"][data-srte-list-depth="2"] > li::marker,
274
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-decimal"][data-srte-list-depth="3"] > li::marker {
275
+ content: counter(list-item, lower-roman) ". ";
276
+ }
277
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-alpha"][data-srte-list-depth="0"] > li::marker {
278
+ content: counter(list-item, upper-alpha) ". ";
279
+ }
280
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-alpha"][data-srte-list-depth="1"] > li::marker {
281
+ content: counter(list-item, lower-alpha) ". ";
282
+ }
283
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-alpha"][data-srte-list-depth="2"] > li::marker,
284
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-alpha"][data-srte-list-depth="3"] > li::marker {
285
+ content: counter(list-item, lower-roman) ". ";
286
+ }
287
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-roman"][data-srte-list-depth="0"] > li::marker {
288
+ content: counter(list-item, upper-roman) ". ";
289
+ }
290
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-roman"][data-srte-list-depth="1"] > li::marker {
291
+ content: counter(list-item, upper-alpha) ". ";
292
+ }
293
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-roman"][data-srte-list-depth="2"] > li::marker,
294
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-upper-roman"][data-srte-list-depth="3"] > li::marker {
295
+ content: counter(list-item, decimal) ". ";
296
+ }
297
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-leading-zero"][data-srte-list-depth="0"] > li::marker {
298
+ content: counter(list-item, decimal-leading-zero) ". ";
299
+ }
300
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-leading-zero"][data-srte-list-depth="1"] > li::marker {
301
+ content: counter(list-item, lower-alpha) ". ";
302
+ }
303
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-leading-zero"][data-srte-list-depth="2"] > li::marker,
304
+ .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-leading-zero"][data-srte-list-depth="3"] > li::marker {
305
+ content: counter(list-item, lower-roman) ". ";
306
+ }
307
+ .srte-editor [contenteditable] ul[data-srte-list-preset="bullet-disc"][data-srte-list-depth="0"] > li::marker {
308
+ content: "● ";
309
+ }
310
+ .srte-editor [contenteditable] ul[data-srte-list-preset="bullet-disc"][data-srte-list-depth="1"] > li::marker {
311
+ content: "○ ";
312
+ }
313
+ .srte-editor [contenteditable] ul[data-srte-list-preset="bullet-disc"][data-srte-list-depth="2"] > li::marker,
314
+ .srte-editor [contenteditable] ul[data-srte-list-preset="bullet-disc"][data-srte-list-depth="3"] > li::marker {
315
+ content: "■ ";
316
+ }
244
317
  .srte-editor [contenteditable] ol[data-srte-list-preset="ordered-outline"] > li::marker {
245
318
  content: counters(list-item, ".") ". ";
246
319
  }
@@ -267,11 +340,26 @@ export const SRTE_DEFAULT_CSS = `
267
340
  justify-content: center;
268
341
  width: 1.1em;
269
342
  height: 1.6em;
343
+ min-width: 0;
344
+ box-sizing: border-box;
345
+ padding: 0;
346
+ border: 0;
347
+ border-radius: 0;
348
+ background: transparent;
349
+ color: inherit;
350
+ font: inherit;
351
+ cursor: pointer;
352
+ appearance: none;
353
+ outline: none;
270
354
  margin: 0;
271
355
  line-height: 1.6;
272
356
  vertical-align: top;
273
357
  position: relative;
274
358
  }
359
+ .srte-editor [contenteditable] ul[data-srte-checklist="true"] > li > [data-srte-check]:focus-visible {
360
+ outline: 2px solid var(--srte-ring);
361
+ outline-offset: 2px;
362
+ }
275
363
  .srte-editor [contenteditable] ul[data-srte-checklist="true"] > li > [data-srte-check]::before {
276
364
  content: "";
277
365
  width: .9em;
@@ -307,25 +395,19 @@ export const SRTE_DEFAULT_CSS = `
307
395
  border-color: var(--srte-ring) !important;
308
396
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--srte-ring) 18%, transparent);
309
397
  }
310
- @media (max-width: 639px) {
311
- .srte-toolbar { gap: 3px; padding: 6px; }
312
- .srte-tool-button, .srte-toolbar select { height: 40px; min-width: 40px; }
313
- .srte-toolbar-group[data-srte-priority="3"] { display: none; }
314
- .srte-toolbar-menu[data-srte-priority="2"] { display: none; }
315
- .srte-mobile-more { display: block; }
316
- .srte-mobile-more .srte-menu {
317
- left: auto;
318
- right: 0;
319
- width: min(280px, calc(100vw - 16px));
320
- min-width: 0;
321
- max-height: min(70dvh, 480px);
322
- overflow-x: hidden;
323
- overflow-y: auto;
324
- overscroll-behavior: contain;
325
- -webkit-overflow-scrolling: touch;
326
- }
327
- .srte-menu-item { height: 40px; }
328
- }
398
+ /*
399
+ * @container (element width), not @media (viewport width): a host can embed
400
+ * the editor at any fraction of the browser window - a split pane next to a
401
+ * preview panel is the case that surfaced this (docs/bugs/
402
+ * toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md). A
403
+ * plain @media breakpoint here would fire based on the *window*, wrongly
404
+ * switching tiers for an editor instance that's actually much narrower (or
405
+ * wider) than the window - .srte-editor already establishes its own
406
+ * container: srte-editor / inline-size (see the .srte-editor rule above),
407
+ * so @container always reflects this specific instance's real rendered width regardless of host
408
+ * layout, with no @media fallback needed (container queries have full
409
+ * support across this project's three target engines).
410
+ */
329
411
  @container srte-editor (max-width: 639px) {
330
412
  .srte-toolbar { gap: 3px; padding: 6px; }
331
413
  .srte-tool-button, .srte-toolbar select { height: 40px; min-width: 40px; }
@@ -345,6 +427,46 @@ export const SRTE_DEFAULT_CSS = `
345
427
  }
346
428
  .srte-menu-item { height: 40px; }
347
429
  }
430
+ /*
431
+ * Wide-viewport promotion (docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md):
432
+ * below this, the single 639px breakpoint above was this system's ONLY
433
+ * threshold - every dropdown-grouped tool stayed hidden inside its dropdown
434
+ * at every width from 640px up to and past a 2200px+ desktop, since nothing
435
+ * ever measured or scaled with the extra room. A handful of tools frequent
436
+ * enough to matter once there's genuinely spare room (Superscript,
437
+ * Subscript, Text colour, Background colour, Font size, Font family, Remove
438
+ * link, Insert formula, Special characters) get a standalone always-visible
439
+ * ToolbarButton copy (data-srte-wide-promote, rendered directly in the
440
+ * toolbar row) that appears past this breakpoint, while their existing
441
+ * dropdown/mobile-menu ToolbarMenuItem copy (the exact same
442
+ * data-srte-wide-promote attribute, on the .srte-menu-item element instead)
443
+ * hides so the tool isn't offered twice. Below this breakpoint - including
444
+ * all of mobile AND the 640-1439px tablet/typical-laptop band, which keeps
445
+ * today's grouped-dropdown layout unchanged - the standalone copy stays
446
+ * hidden and the dropdown copy is what's reachable.
447
+ *
448
+ * 1440px, not 1280px: Playwright's own default test viewport is exactly
449
+ * 1280x720, and the vast majority of this suite's toolbar interactions run
450
+ * at that default without ever calling page.setViewportSize - a 1280px
451
+ * threshold would have flipped nearly every existing toolbar test's
452
+ * dropdown-item locators (role=menuitem) over to CSS display:none out from
453
+ * under them. 1440px clears that default with room to spare while still
454
+ * comfortably covering "wide desktop" (the ~2264px width the original
455
+ * report was filed against).
456
+ *
457
+ * @container only, no @media fallback - same reasoning as the mobile tier
458
+ * above (docs/bugs/toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md):
459
+ * a plain @media(min-width:1440px) fired whenever the *browser window* was
460
+ * wide, even when this editor instance was embedded in a much narrower
461
+ * split pane - promoting 9 extra buttons into a group with no room for
462
+ * them, which squeezed/overlapped the group's existing buttons instead of
463
+ * wrapping cleanly.
464
+ */
465
+ .srte-tool-button[data-srte-wide-promote="true"] { display: none; }
466
+ @container srte-editor (min-width: 1440px) {
467
+ .srte-tool-button[data-srte-wide-promote="true"] { display: inline-flex; }
468
+ .srte-menu-item[data-srte-wide-promote="true"] { display: none; }
469
+ }
348
470
  .srte-editor [contenteditable] blockquote {
349
471
  border-left: 4px solid var(--srte-accent);
350
472
  margin: 0.75em 0;
@@ -419,6 +541,14 @@ export const SRTE_DEFAULT_CSS = `
419
541
  border: 1px solid var(--srte-border);
420
542
  vertical-align: top;
421
543
  }
544
+ .srte-editor [contenteditable] [data-smart-cell-selected="true"] {
545
+ background: color-mix(in srgb, var(--srte-primary) 12%, var(--srte-canvas));
546
+ box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--srte-primary) 65%, transparent);
547
+ }
548
+ .srte-editor [contenteditable][data-smart-cell-selection-active] ::selection {
549
+ background: transparent;
550
+ color: inherit;
551
+ }
422
552
  .srte-editor [contenteditable] th {
423
553
  background: var(--srte-surface-subtle);
424
554
  font-weight: 600;
@@ -0,0 +1,29 @@
1
+ import type { DocumentVersion } from "smartrte-core/foundation";
2
+ /**
3
+ * The listing-cheap projection of a `DocumentVersion` - a host's backend
4
+ * shouldn't have to ship every saved version's full document body just to
5
+ * render a history list. Mirrors `MediaProvider.search` returning
6
+ * `MediaItem` (metadata) rather than raw asset bytes.
7
+ */
8
+ export interface VersionListEntry {
9
+ readonly id: string;
10
+ readonly label?: string;
11
+ readonly createdAt: number;
12
+ readonly authorId?: string;
13
+ }
14
+ /**
15
+ * Host-owned version-storage boundary, mirroring `MediaProvider`'s shape
16
+ * and its "the editor never persists anything itself" contract: the host
17
+ * backend is the source of truth, and `save`/`list`/`load`/`remove` are
18
+ * the entire boundary. There is deliberately no shipped default
19
+ * implementation, exactly like `MediaProvider` - when a host doesn't
20
+ * supply one, the version-history feature is simply unavailable, the same
21
+ * way the media toolbar buttons are disabled when `mediaProvider` is
22
+ * absent.
23
+ */
24
+ export interface VersionProvider {
25
+ save(version: DocumentVersion): Promise<VersionListEntry>;
26
+ list(): Promise<readonly VersionListEntry[]>;
27
+ load(id: string): Promise<DocumentVersion>;
28
+ remove(id: string): Promise<void>;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,15 +1,26 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "0.3.5",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./standalone/classic-editor-embed": {
15
+ "types": "./dist/standalone/classic-editor-embed.d.ts",
16
+ "import": "./dist/standalone/classic-editor-embed.js"
17
+ }
18
+ },
9
19
  "files": [
10
20
  "dist",
11
21
  "!dist/**/*.test.js",
12
22
  "!dist/**/*.test.d.ts",
23
+ "!dist/test-harness/**",
13
24
  "README.md",
14
25
  "CHANGELOG.md"
15
26
  ],
@@ -41,16 +52,6 @@
41
52
  },
42
53
  "author": "Smart RTE Contributors",
43
54
  "license": "MIT",
44
- "scripts": {
45
- "build": "tsc -p tsconfig.json",
46
- "prepublishOnly": "npm run build",
47
- "dev": "pnpm build",
48
- "lint": "tsc -p tsconfig.json --noEmit",
49
- "test": "vitest run --passWithNoTests",
50
- "storybook": "storybook dev -p 6006",
51
- "build-storybook": "storybook build",
52
- "e2e": "playwright test --pass-with-no-tests"
53
- },
54
55
  "publishConfig": {
55
56
  "access": "public"
56
57
  },
@@ -60,16 +61,18 @@
60
61
  "react-dom": ">=18"
61
62
  },
62
63
  "devDependencies": {
64
+ "@axe-core/playwright": "^4.12.1",
63
65
  "@playwright/test": "^1.48.2",
64
66
  "@storybook/addon-essentials": "^8.6.14",
65
67
  "@storybook/react": "^8.6.14",
66
68
  "@storybook/react-vite": "^8.6.14",
69
+ "@types/node": "^22",
67
70
  "@types/pdfjs-dist": "^2.10.377",
68
71
  "@types/react": "^18.3.4",
69
72
  "@types/react-dom": "^18.3.0",
73
+ "jsdom": "^25.0.1",
70
74
  "react": "18.3.1",
71
75
  "react-dom": "18.3.1",
72
- "jsdom": "^25.0.1",
73
76
  "storybook": "^8.6.14",
74
77
  "typescript": "^5.6.2",
75
78
  "vite": "^5.4.8",
@@ -77,8 +80,19 @@
77
80
  },
78
81
  "dependencies": {
79
82
  "jszip": "^3.10.1",
83
+ "katex": "^0.18.4",
84
+ "lucide-react": "^1.35.0",
80
85
  "mammoth": "^1.11.0",
81
86
  "pdfjs-dist": "^5.4.530",
82
- "smartrte-core": "^0.2.1"
87
+ "smartrte-core": "^1.0.0-beta.1"
88
+ },
89
+ "scripts": {
90
+ "build": "rm -rf dist && tsc -p tsconfig.json",
91
+ "dev": "pnpm build",
92
+ "lint": "tsc -p tsconfig.json --noEmit",
93
+ "test": "vitest run --passWithNoTests",
94
+ "storybook": "storybook dev -p 6006",
95
+ "build-storybook": "storybook build",
96
+ "e2e": "pnpm --filter smartrte-core build && pnpm build && playwright test --pass-with-no-tests"
83
97
  }
84
- }
98
+ }
@@ -1,15 +0,0 @@
1
- import { inlineToolDeclarations, type Attrs, type SmartMark } from "smartrte-core";
2
- export type CanonicalInlineToolId = typeof inlineToolDeclarations[number]["id"];
3
- export type CanonicalInlineIntent = "apply" | "remove" | "toggle" | "setAttrs" | "clearAll" | "editLink";
4
- export interface CanonicalInlineExecution {
5
- readonly changed: boolean;
6
- readonly operations: number;
7
- readonly coverage: "all" | "partial" | "none";
8
- }
9
- export declare const executeCanonicalInlineTool: (root: HTMLElement, id: CanonicalInlineToolId, intent?: CanonicalInlineIntent, attrs?: Attrs) => CanonicalInlineExecution;
10
- /** Consumes adapter stored marks for ordinary text input. Composition remains
11
- * owned by the canonical surface; atom-aware composition is explicitly Phase 7. */
12
- export declare const installCanonicalInlineStoredMarkInput: (root: HTMLElement) => (() => void);
13
- export declare const canonicalInlineStoredMarks: (root: HTMLElement) => readonly SmartMark[];
14
- export declare const hasCanonicalInlineStoredMarkOverride: (root: HTMLElement) => boolean;
15
- export declare const describeCanonicalInlineCoverage: (root: HTMLElement) => Readonly<Record<string, "all" | "partial">>;
@@ -1,269 +0,0 @@
1
- import { applyMarkCommand, applyOperations, clearAllMarksCommand, createFoundationEditor, createNodeId, createScopeIndex, editLinkCommand, executeMarkTool, foundationSchema, inlineToolDeclarations, parseCanonicalListHtml, removeLinkCommand, removeMarkCommand, serializeCanonicalListHtml, toggleMarkCommand, } from "smartrte-core";
2
- const storedMarks = new WeakMap();
3
- const ownerSelector = "p,h1,h2,h3,h4,h5,h6";
4
- const ownerId = (owner) => {
5
- var _a;
6
- (_a = owner.dataset).smartId || (_a.smartId = createNodeId());
7
- return owner.dataset.smartId;
8
- };
9
- const contentWidth = (node) => {
10
- if (node.nodeType === node.TEXT_NODE)
11
- return node.nodeValue?.length || 0;
12
- if (!(node instanceof HTMLElement))
13
- return [...node.childNodes].reduce((sum, child) => sum + contentWidth(child), 0);
14
- if (node.closest("[data-smart-ui]"))
15
- return 0;
16
- if (node.matches("br,[data-smart-atomic='true']"))
17
- return 1;
18
- return [...node.childNodes].reduce((sum, child) => sum + contentWidth(child), 0);
19
- };
20
- const pointInOwner = (root, node, offset) => {
21
- const element = node instanceof HTMLElement ? node : node?.parentElement;
22
- const owner = element?.closest(ownerSelector);
23
- if (!owner || !root.contains(owner) || !node)
24
- return null;
25
- const range = root.ownerDocument.createRange();
26
- range.selectNodeContents(owner);
27
- try {
28
- range.setEnd(node, offset);
29
- }
30
- catch {
31
- return null;
32
- }
33
- return { ownerId: ownerId(owner), offset: contentWidth(range.cloneContents()) };
34
- };
35
- const captureSelection = (root) => {
36
- const native = root.ownerDocument.defaultView?.getSelection();
37
- if (!native?.anchorNode || !native.focusNode)
38
- return null;
39
- const anchor = pointInOwner(root, native.anchorNode, native.anchorOffset);
40
- const head = pointInOwner(root, native.focusNode, native.focusOffset);
41
- return anchor && head ? { anchor, head } : null;
42
- };
43
- const domPointAt = (owner, requested) => {
44
- let remaining = Math.max(0, requested);
45
- const visit = (parent) => {
46
- for (let index = 0; index < parent.childNodes.length; index += 1) {
47
- const child = parent.childNodes[index];
48
- if (child.nodeType === child.TEXT_NODE) {
49
- const length = child.nodeValue?.length || 0;
50
- if (remaining <= length)
51
- return { node: child, offset: remaining };
52
- remaining -= length;
53
- }
54
- else if (child instanceof HTMLElement && child.matches("br,[data-smart-atomic='true']")) {
55
- if (remaining === 0)
56
- return { node: parent, offset: index };
57
- if (remaining === 1)
58
- return { node: parent, offset: index + 1 };
59
- remaining -= 1;
60
- }
61
- else if (!(child instanceof HTMLElement && child.closest("[data-smart-ui]"))) {
62
- const found = visit(child);
63
- if (found)
64
- return found;
65
- }
66
- }
67
- return null;
68
- };
69
- return visit(owner) || { node: owner, offset: owner.childNodes.length };
70
- };
71
- const restoreSelection = (root, snapshot) => {
72
- const byId = (id) => Array.from(root.querySelectorAll("[data-smart-id]"))
73
- .find((node) => node.matches(ownerSelector) && node.dataset.smartId === id) || null;
74
- const anchorOwner = byId(snapshot.anchor.ownerId);
75
- const headOwner = byId(snapshot.head.ownerId);
76
- const native = root.ownerDocument.defaultView?.getSelection();
77
- if (!anchorOwner || !headOwner || !native)
78
- return;
79
- const anchor = domPointAt(anchorOwner, snapshot.anchor.offset);
80
- const head = domPointAt(headOwner, snapshot.head.offset);
81
- native.setBaseAndExtent(anchor.node, anchor.offset, head.node, head.offset);
82
- };
83
- const selectedOwners = (root, snapshot) => {
84
- const owners = Array.from(root.querySelectorAll(ownerSelector));
85
- const indexes = [snapshot.anchor.ownerId, snapshot.head.ownerId].map((id) => owners.findIndex((owner) => ownerId(owner) === id));
86
- if (indexes.some((index) => index < 0))
87
- return [];
88
- const [from, to] = [Math.min(...indexes), Math.max(...indexes)];
89
- return owners.slice(from, to + 1);
90
- };
91
- const modelSelection = (document, snapshot) => {
92
- const positions = createScopeIndex().positions(document, foundationSchema);
93
- const point = (value) => {
94
- const content = positions.contentRangeOf(value.ownerId);
95
- return content ? { path: [...content.from.path], offset: value.offset } : null;
96
- };
97
- const anchor = point(snapshot.anchor);
98
- const head = point(snapshot.head);
99
- return anchor && head ? { type: "text", anchor, head } : null;
100
- };
101
- const parseOwners = (owners) => parseCanonicalListHtml(owners.map((owner) => owner.outerHTML).join(""));
102
- const renderOwners = (root, owners, document, preserveEditedLink = false) => {
103
- const template = root.ownerDocument.createElement("template");
104
- template.innerHTML = serializeCanonicalListHtml(document, { fragment: true });
105
- owners.forEach((owner) => {
106
- const id = ownerId(owner);
107
- const replacement = Array.from(template.content.querySelectorAll("[data-smart-id]"))
108
- .find((candidate) => candidate.dataset.smartId === id);
109
- if (replacement) {
110
- const existingLinks = owner.querySelectorAll("a");
111
- const replacementLinks = replacement.querySelectorAll("a");
112
- if (preserveEditedLink && existingLinks.length === 1 && replacementLinks.length === 1
113
- && existingLinks[0].textContent === replacementLinks[0].textContent
114
- && owner.textContent === replacement.textContent) {
115
- const next = replacementLinks[0];
116
- existingLinks[0].setAttribute("href", next.getAttribute("href") || "");
117
- if (next.target) {
118
- existingLinks[0].target = next.target;
119
- existingLinks[0].rel = "noopener noreferrer";
120
- }
121
- else {
122
- existingLinks[0].removeAttribute("target");
123
- existingLinks[0].removeAttribute("rel");
124
- }
125
- return;
126
- }
127
- owner.innerHTML = replacement.innerHTML;
128
- owner.querySelectorAll("span[style]").forEach((span) => {
129
- const color = span.style.color;
130
- const background = span.style.backgroundColor;
131
- const size = span.style.fontSize;
132
- const family = span.style.fontFamily;
133
- span.removeAttribute("style");
134
- if (color)
135
- span.style.color = color;
136
- if (background)
137
- span.style.backgroundColor = background;
138
- if (size)
139
- span.style.fontSize = size;
140
- if (family)
141
- span.style.fontFamily = family;
142
- });
143
- }
144
- });
145
- };
146
- const declarationFor = (id) => {
147
- const declaration = inlineToolDeclarations.find((tool) => tool.id === id);
148
- if (!declaration)
149
- throw new Error(`Unknown inline tool "${id}".`);
150
- return declaration;
151
- };
152
- export const executeCanonicalInlineTool = (root, id, intent = "toggle", attrs) => {
153
- const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
154
- const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
155
- const cleanupTemporaryIds = () => ownerElements.forEach((owner) => {
156
- if (!ownersWithPersistedIds.has(owner))
157
- owner.removeAttribute("data-smart-id");
158
- });
159
- const snapshot = captureSelection(root);
160
- if (!snapshot)
161
- return { changed: false, operations: 0, coverage: "none" };
162
- const owners = selectedOwners(root, snapshot);
163
- if (!owners.length)
164
- return { changed: false, operations: 0, coverage: "none" };
165
- const document = parseOwners(owners);
166
- const selection = modelSelection(document, snapshot);
167
- if (!selection)
168
- return { changed: false, operations: 0, coverage: "none" };
169
- const declaration = declarationFor(id);
170
- const index = createScopeIndex();
171
- const scope = index.resolve(document, selection, { want: "inline-range" }, foundationSchema);
172
- const description = index.resolve(document, selection, { want: "describe" }, foundationSchema);
173
- const coverage = description.marks.some((entry) => entry.mark.type === declaration.markType && entry.coverage === "all")
174
- ? "all" : description.marks.some((entry) => entry.mark.type === declaration.markType) ? "partial" : "none";
175
- if (scope.collapsed && !(declaration.markType === "link" && (intent === "remove" || intent === "editLink"))) {
176
- const editor = createFoundationEditor({ document, selection, storedMarks: storedMarks.get(root) });
177
- executeMarkTool(editor, declaration, intent, attrs);
178
- storedMarks.set(root, [...(editor.storedMarks || [])]);
179
- cleanupTemporaryIds();
180
- return { changed: false, operations: 0, coverage };
181
- }
182
- const ctx = { schema: foundationSchema, positions: index.positions(document, foundationSchema) };
183
- const operations = intent === "clearAll" ? clearAllMarksCommand(document, scope, {}, ctx)
184
- : intent === "remove" && declaration.markType === "link" ? removeLinkCommand(document, scope, { markType: "link" }, ctx)
185
- : intent === "editLink" && declaration.markType === "link" ? editLinkCommand(document, scope, attrs, ctx)
186
- : intent === "remove" ? removeMarkCommand(document, scope, { markType: declaration.markType }, ctx)
187
- : intent === "toggle" ? toggleMarkCommand(document, scope, { markType: declaration.markType, attrs, coverage }, ctx)
188
- : applyMarkCommand(document, scope, { markType: declaration.markType, attrs }, ctx);
189
- if (!operations.length)
190
- return { changed: false, operations: 0, coverage };
191
- const output = applyOperations(document, operations);
192
- renderOwners(root, owners, output, intent === "editLink");
193
- restoreSelection(root, snapshot);
194
- cleanupTemporaryIds();
195
- storedMarks.delete(root);
196
- return { changed: true, operations: operations.length, coverage };
197
- };
198
- /** Consumes adapter stored marks for ordinary text input. Composition remains
199
- * owned by the canonical surface; atom-aware composition is explicitly Phase 7. */
200
- export const installCanonicalInlineStoredMarkInput = (root) => {
201
- const beforeInput = (event) => {
202
- if (!storedMarks.has(root) || event.inputType !== "insertText" || !event.data)
203
- return;
204
- const marks = storedMarks.get(root) || [];
205
- const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
206
- const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
207
- const snapshot = captureSelection(root);
208
- if (!snapshot || snapshot.anchor.ownerId !== snapshot.head.ownerId || snapshot.anchor.offset !== snapshot.head.offset)
209
- return;
210
- event.preventDefault();
211
- const owners = selectedOwners(root, snapshot);
212
- const document = parseOwners(owners);
213
- const selection = modelSelection(document, snapshot);
214
- if (!selection)
215
- return;
216
- const editor = createFoundationEditor({ document, selection, storedMarks: marks });
217
- if (marks.length)
218
- editor.typeText(event.data, { timestamp: Date.now() });
219
- else
220
- editor.transact((builder) => {
221
- builder.operations.push({ type: "insertText", pos: selection.head, text: event.data });
222
- const next = { path: [...selection.head.path], offset: selection.head.offset + event.data.length };
223
- builder.setSelection({ type: "text", anchor: next, head: next });
224
- builder.setStoredMarks([]);
225
- }, { source: "input", timestamp: Date.now(), addToHistory: true });
226
- renderOwners(root, owners, editor.document);
227
- const next = { anchor: { ...snapshot.anchor, offset: snapshot.anchor.offset + event.data.length }, head: { ...snapshot.head, offset: snapshot.head.offset + event.data.length } };
228
- restoreSelection(root, next);
229
- ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
230
- owner.removeAttribute("data-smart-id"); });
231
- storedMarks.set(root, [...(editor.storedMarks || [])]);
232
- };
233
- const selectionChange = () => {
234
- const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
235
- const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
236
- const snapshot = captureSelection(root);
237
- if (!snapshot || snapshot.anchor.ownerId !== snapshot.head.ownerId || snapshot.anchor.offset !== snapshot.head.offset)
238
- storedMarks.delete(root);
239
- ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
240
- owner.removeAttribute("data-smart-id"); });
241
- };
242
- root.addEventListener("beforeinput", beforeInput);
243
- root.ownerDocument.addEventListener("selectionchange", selectionChange);
244
- return () => {
245
- root.removeEventListener("beforeinput", beforeInput);
246
- root.ownerDocument.removeEventListener("selectionchange", selectionChange);
247
- storedMarks.delete(root);
248
- };
249
- };
250
- export const canonicalInlineStoredMarks = (root) => storedMarks.get(root) || [];
251
- export const hasCanonicalInlineStoredMarkOverride = (root) => storedMarks.has(root);
252
- export const describeCanonicalInlineCoverage = (root) => {
253
- if (storedMarks.has(root))
254
- return Object.fromEntries((storedMarks.get(root) || []).map((mark) => [mark.type, "all"]));
255
- const ownerElements = Array.from(root.querySelectorAll(ownerSelector));
256
- const ownersWithPersistedIds = new Set(ownerElements.filter((owner) => owner.hasAttribute("data-smart-id")));
257
- const snapshot = captureSelection(root);
258
- if (!snapshot)
259
- return {};
260
- const owners = selectedOwners(root, snapshot);
261
- const document = parseOwners(owners);
262
- const selection = modelSelection(document, snapshot);
263
- const description = selection
264
- ? createScopeIndex().resolve(document, selection, { want: "describe" }, foundationSchema)
265
- : null;
266
- ownerElements.forEach((owner) => { if (!ownersWithPersistedIds.has(owner))
267
- owner.removeAttribute("data-smart-id"); });
268
- return Object.fromEntries((description?.marks || []).map((entry) => [entry.mark.type, entry.coverage]));
269
- };
@@ -1,26 +0,0 @@
1
- import { type SmartSelection } from "smartrte-core";
2
- export declare const executeCanonicalListDepth: (root: HTMLElement, items: readonly HTMLElement[], direction: "indent" | "outdent") => boolean;
3
- export declare const executeCanonicalListMove: (root: HTMLElement, items: readonly HTMLElement[], direction: "up" | "down") => boolean;
4
- export declare const executeCanonicalListStyle: (args: {
5
- root: HTMLElement;
6
- items: readonly HTMLElement[];
7
- style?: string;
8
- preset?: string;
9
- checkable?: boolean;
10
- }) => boolean;
11
- export declare const executeCanonicalListCheck: (root: HTMLElement, item: HTMLElement, checked: boolean) => boolean;
12
- export declare const executeCanonicalListToggle: (args: {
13
- root: HTMLElement;
14
- items: readonly HTMLElement[];
15
- blocks: readonly HTMLElement[];
16
- listTag: "ul" | "ol";
17
- range?: Range;
18
- style?: string;
19
- preset?: string;
20
- checkable?: boolean;
21
- }) => boolean;
22
- export interface CanonicalListBridgeResult {
23
- readonly changed: boolean;
24
- readonly selection: SmartSelection | null;
25
- }
26
- export declare const executeCanonicalListStructuralInput: (root: HTMLElement, input: "enter" | "backspace" | "delete") => boolean;