@apollohg/react-native-prose-editor 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -1,6 +1,5 @@
1
1
  import ExpoModulesCore
2
2
  import UIKit
3
- import os
4
3
 
5
4
  private struct NativeToolbarState {
6
5
  let marks: [String: Bool]
@@ -91,6 +90,9 @@ private enum ToolbarDefaultIconId: String {
91
90
  case italic
92
91
  case underline
93
92
  case strike
93
+ case link
94
+ case image
95
+ case blockquote
94
96
  case bulletList
95
97
  case orderedList
96
98
  case indentList
@@ -103,6 +105,7 @@ private enum ToolbarDefaultIconId: String {
103
105
 
104
106
  private enum ToolbarItemKind: String {
105
107
  case mark
108
+ case blockquote
106
109
  case list
107
110
  case command
108
111
  case node
@@ -121,6 +124,9 @@ private struct NativeToolbarIcon {
121
124
  .italic: "italic",
122
125
  .underline: "underline",
123
126
  .strike: "strikethrough",
127
+ .link: "link",
128
+ .image: "photo",
129
+ .blockquote: "text.quote",
124
130
  .bulletList: "list.bullet",
125
131
  .orderedList: "list.number",
126
132
  .indentList: "increase.indent",
@@ -136,6 +142,9 @@ private struct NativeToolbarIcon {
136
142
  .italic: "I",
137
143
  .underline: "U",
138
144
  .strike: "S",
145
+ .link: "🔗",
146
+ .image: "🖼",
147
+ .blockquote: "❝",
139
148
  .bulletList: "•≡",
140
149
  .orderedList: "1.",
141
150
  .indentList: "→",
@@ -240,6 +249,7 @@ private struct NativeToolbarItem {
240
249
  NativeToolbarItem(type: .mark, key: nil, label: "Italic", icon: .defaultIcon(.italic), mark: "italic", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
241
250
  NativeToolbarItem(type: .mark, key: nil, label: "Underline", icon: .defaultIcon(.underline), mark: "underline", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
242
251
  NativeToolbarItem(type: .mark, key: nil, label: "Strikethrough", icon: .defaultIcon(.strike), mark: "strike", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
252
+ NativeToolbarItem(type: .blockquote, key: nil, label: "Blockquote", icon: .defaultIcon(.blockquote), mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
243
253
  NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
244
254
  NativeToolbarItem(type: .list, key: nil, label: "Bullet List", icon: .defaultIcon(.bulletList), mark: nil, listType: .bulletList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
245
255
  NativeToolbarItem(type: .list, key: nil, label: "Ordered List", icon: .defaultIcon(.orderedList), mark: nil, listType: .orderedList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
@@ -301,6 +311,24 @@ private struct NativeToolbarItem {
301
311
  isActive: false,
302
312
  isDisabled: false
303
313
  )
314
+ case .blockquote:
315
+ guard let label = rawItem["label"] as? String,
316
+ let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
317
+ else {
318
+ return nil
319
+ }
320
+ return NativeToolbarItem(
321
+ type: .blockquote,
322
+ key: key,
323
+ label: label,
324
+ icon: icon,
325
+ mark: nil,
326
+ listType: nil,
327
+ command: nil,
328
+ nodeType: nil,
329
+ isActive: false,
330
+ isDisabled: false
331
+ )
304
332
  case .list:
305
333
  guard let listTypeRaw = rawItem["listType"] as? String,
306
334
  let listType = ToolbarListType(rawValue: listTypeRaw),
@@ -392,6 +420,8 @@ private struct NativeToolbarItem {
392
420
  switch type {
393
421
  case .mark:
394
422
  return "mark:\(mark ?? ""):\(index)"
423
+ case .blockquote:
424
+ return "blockquote:\(index)"
395
425
  case .list:
396
426
  return "list:\(listType?.rawValue ?? ""):\(index)"
397
427
  case .command:
@@ -406,7 +436,7 @@ private struct NativeToolbarItem {
406
436
  }
407
437
  }
408
438
 
409
- final class EditorAccessoryToolbarView: UIView {
439
+ final class EditorAccessoryToolbarView: UIInputView {
410
440
  private static let baseHeight: CGFloat = 50
411
441
  private static let mentionRowHeight: CGFloat = 52
412
442
  private static let contentSpacing: CGFloat = 6
@@ -418,7 +448,16 @@ final class EditorAccessoryToolbarView: UIView {
418
448
  let button: UIButton
419
449
  }
420
450
 
451
+ private struct BarButtonBinding {
452
+ let item: NativeToolbarItem
453
+ let button: UIBarButtonItem
454
+ }
455
+
421
456
  private let chromeView = UIView()
457
+ private let blurView = UIVisualEffectView(effect: nil)
458
+ private let glassTintView = UIView()
459
+ private let nativeToolbarScrollView = UIScrollView()
460
+ private let nativeToolbarView = UIToolbar()
422
461
  private let contentStackView = UIStackView()
423
462
  private let mentionScrollView = UIScrollView()
424
463
  private let mentionStackView = UIStackView()
@@ -427,8 +466,11 @@ final class EditorAccessoryToolbarView: UIView {
427
466
  private var chromeLeadingConstraint: NSLayoutConstraint?
428
467
  private var chromeTrailingConstraint: NSLayoutConstraint?
429
468
  private var chromeBottomConstraint: NSLayoutConstraint?
469
+ private var nativeToolbarWidthConstraint: NSLayoutConstraint?
430
470
  private var mentionRowHeightConstraint: NSLayoutConstraint?
471
+ private var nativeToolbarDidInitializeScrollPosition = false
431
472
  private var buttonBindings: [ButtonBinding] = []
473
+ private var barButtonBindings: [BarButtonBinding] = []
432
474
  private var separators: [UIView] = []
433
475
  private var mentionButtons: [MentionSuggestionChipButton] = []
434
476
  private var items: [NativeToolbarItem] = NativeToolbarItem.defaults
@@ -440,20 +482,68 @@ final class EditorAccessoryToolbarView: UIView {
440
482
  var isShowingMentionSuggestions: Bool {
441
483
  !mentionButtons.isEmpty && !mentionScrollView.isHidden && scrollView.isHidden
442
484
  }
485
+ var usesNativeAppearanceForTesting: Bool {
486
+ resolvedAppearance == .native
487
+ }
488
+ var usesUIGlassEffectForTesting: Bool {
489
+ #if compiler(>=6.2)
490
+ if #available(iOS 26.0, *) {
491
+ return blurView.effect is UIGlassEffect
492
+ }
493
+ #endif
494
+ return false
495
+ }
496
+ var chromeBorderWidthForTesting: CGFloat {
497
+ chromeView.layer.borderWidth
498
+ }
499
+ var nativeToolbarVisibleWidthForTesting: CGFloat {
500
+ activeNativeToolbarScrollViewForTesting.bounds.width
501
+ }
502
+ var nativeToolbarContentWidthForTesting: CGFloat {
503
+ if usesNativeBarToolbar {
504
+ return max(nativeToolbarScrollView.contentSize.width, nativeToolbarView.bounds.width)
505
+ }
506
+ return max(scrollView.contentSize.width, stackView.bounds.width)
507
+ }
508
+ var nativeToolbarContentOffsetXForTesting: CGFloat {
509
+ activeNativeToolbarScrollViewForTesting.contentOffset.x
510
+ }
511
+ func setNativeToolbarContentOffsetXForTesting(_ offsetX: CGFloat) {
512
+ activeNativeToolbarScrollViewForTesting.contentOffset.x = offsetX
513
+ }
514
+ var selectedButtonCountForTesting: Int {
515
+ #if compiler(>=6.2)
516
+ if #available(iOS 26.0, *) {
517
+ if usesNativeBarToolbar {
518
+ return barButtonBindings.filter { $0.button.style == .prominent }.count
519
+ }
520
+ }
521
+ #endif
522
+ return buttonBindings.filter(\.button.isSelected).count
523
+ }
524
+ func mentionButtonAtForTesting(_ index: Int) -> MentionSuggestionChipButton? {
525
+ mentionButtons.indices.contains(index) ? mentionButtons[index] : nil
526
+ }
443
527
 
444
528
  override var intrinsicContentSize: CGSize {
445
529
  let contentHeight = mentionButtons.isEmpty ? Self.baseHeight : Self.mentionRowHeight
446
530
  return CGSize(
447
531
  width: UIView.noIntrinsicMetric,
448
- height: contentHeight + (theme?.keyboardOffset ?? Self.defaultKeyboardOffset)
532
+ height: contentHeight + resolvedKeyboardOffset
449
533
  )
450
534
  }
451
535
 
452
- override init(frame: CGRect) {
453
- super.init(frame: frame)
536
+ convenience init(frame: CGRect) {
537
+ self.init(frame: frame, inputViewStyle: .keyboard)
538
+ }
539
+
540
+ override init(frame: CGRect, inputViewStyle: UIInputView.Style) {
541
+ super.init(frame: frame, inputViewStyle: inputViewStyle)
454
542
  translatesAutoresizingMaskIntoConstraints = false
455
543
  autoresizingMask = [.flexibleHeight]
456
544
  backgroundColor = .clear
545
+ isOpaque = false
546
+ allowsSelfSizing = true
457
547
  setupView()
458
548
  rebuildButtons()
459
549
  }
@@ -462,6 +552,21 @@ final class EditorAccessoryToolbarView: UIView {
462
552
  return nil
463
553
  }
464
554
 
555
+ override func didMoveToSuperview() {
556
+ super.didMoveToSuperview()
557
+ refreshNativeHostTransparencyIfNeeded()
558
+ }
559
+
560
+ override func didMoveToWindow() {
561
+ super.didMoveToWindow()
562
+ refreshNativeHostTransparencyIfNeeded()
563
+ }
564
+
565
+ override func layoutSubviews() {
566
+ super.layoutSubviews()
567
+ updateNativeToolbarMetricsIfNeeded()
568
+ }
569
+
465
570
  fileprivate func setItems(_ items: [NativeToolbarItem]) {
466
571
  self.items = items
467
572
  rebuildButtons()
@@ -476,24 +581,72 @@ final class EditorAccessoryToolbarView: UIView {
476
581
 
477
582
  func apply(theme: EditorToolbarTheme?) {
478
583
  self.theme = theme
479
- chromeView.backgroundColor = theme?.backgroundColor ?? .systemBackground
480
- chromeView.layer.borderColor = (theme?.borderColor ?? UIColor.separator).cgColor
481
- chromeView.layer.borderWidth = theme?.borderWidth ?? 0.5
482
- chromeView.layer.cornerRadius = theme?.borderRadius ?? 0
483
- chromeView.clipsToBounds = (theme?.borderRadius ?? 0) > 0
484
- chromeLeadingConstraint?.constant = theme?.horizontalInset ?? Self.defaultHorizontalInset
485
- chromeTrailingConstraint?.constant = -(theme?.horizontalInset ?? Self.defaultHorizontalInset)
486
- chromeBottomConstraint?.constant = -(theme?.keyboardOffset ?? Self.defaultKeyboardOffset)
584
+ let usesNativeAppearance = resolvedAppearance == .native
585
+ let hasFloatingGlassButtons = self.usesFloatingGlassButtons
586
+ let usesBarToolbar = usesNativeBarToolbar
587
+ chromeView.backgroundColor = usesNativeAppearance
588
+ ? .clear
589
+ : (theme?.backgroundColor ?? .systemBackground)
590
+ chromeView.tintColor = usesNativeAppearance
591
+ ? nil
592
+ : (theme?.buttonColor ?? tintColor)
593
+ chromeView.isOpaque = false
594
+ blurView.isHidden = usesBarToolbar || !usesNativeAppearance
595
+ blurView.effect = usesNativeAppearance ? resolvedBlurEffect() : nil
596
+ blurView.alpha = usesNativeAppearance ? resolvedEffectAlpha : 1
597
+ glassTintView.isHidden = usesBarToolbar || !usesNativeAppearance
598
+ glassTintView.backgroundColor = usesNativeAppearance
599
+ ? UIColor.systemBackground.withAlphaComponent(resolvedGlassTintAlpha)
600
+ : .clear
601
+ chromeView.layer.borderColor = resolvedBorderColor.cgColor
602
+ chromeView.layer.borderWidth = usesBarToolbar
603
+ ? 0
604
+ : (usesNativeAppearance
605
+ ? (1 / UIScreen.main.scale)
606
+ : resolvedBorderWidth)
607
+ chromeView.layer.cornerRadius = resolvedBorderRadius
608
+ if #available(iOS 13.0, *) {
609
+ chromeView.layer.cornerCurve = .continuous
610
+ }
611
+ #if compiler(>=6.2)
612
+ if #available(iOS 26.0, *) {
613
+ let cornerConfig: UICornerConfiguration = usesNativeAppearance
614
+ ? .capsule(maximumRadius: 24)
615
+ : .uniformCorners(radius: .fixed(Double(resolvedBorderRadius)))
616
+ chromeView.cornerConfiguration = cornerConfig
617
+ blurView.cornerConfiguration = cornerConfig
618
+ glassTintView.cornerConfiguration = cornerConfig
619
+ }
620
+ #endif
621
+ chromeView.clipsToBounds = (usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar) || resolvedBorderRadius > 0
622
+ chromeView.layer.shadowOpacity = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 0.08 : 0
623
+ chromeView.layer.shadowRadius = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 10 : 0
624
+ chromeView.layer.shadowOffset = CGSize(width: 0, height: 2)
625
+ chromeView.layer.shadowColor = UIColor.black.cgColor
626
+ chromeLeadingConstraint?.constant = resolvedHorizontalInset
627
+ chromeTrailingConstraint?.constant = -resolvedHorizontalInset
628
+ chromeBottomConstraint?.constant = -resolvedKeyboardOffset
629
+ nativeToolbarScrollView.isHidden = !(usesBarToolbar && mentionButtons.isEmpty)
630
+ nativeToolbarView.isHidden = !(usesBarToolbar && mentionButtons.isEmpty)
631
+ nativeToolbarView.tintColor = usesNativeAppearance
632
+ ? nil
633
+ : (theme?.buttonColor ?? tintColor)
634
+ contentStackView.isHidden = usesBarToolbar && mentionButtons.isEmpty
487
635
  invalidateIntrinsicContentSize()
488
636
  for separator in separators {
489
- separator.backgroundColor = theme?.separatorColor ?? .separator
637
+ separator.isHidden = hasFloatingGlassButtons
638
+ separator.backgroundColor = usesNativeAppearance
639
+ ? UIColor.separator.withAlphaComponent(0.45)
640
+ : (theme?.separatorColor ?? .separator)
490
641
  }
491
642
  for binding in buttonBindings {
492
- binding.button.layer.cornerRadius = theme?.buttonBorderRadius ?? 8
643
+ binding.button.layer.cornerRadius = resolvedButtonBorderRadius
493
644
  }
494
645
  for button in mentionButtons {
495
- button.apply(theme: mentionTheme)
646
+ button.apply(theme: mentionTheme, toolbarAppearance: resolvedAppearance)
496
647
  }
648
+ refreshNativeHostTransparencyIfNeeded()
649
+ updateNativeToolbarMetricsIfNeeded()
497
650
  apply(state: currentState)
498
651
  }
499
652
 
@@ -508,7 +661,11 @@ final class EditorAccessoryToolbarView: UIView {
508
661
  mentionButtons.removeAll()
509
662
 
510
663
  for suggestion in suggestions.prefix(8) {
511
- let button = MentionSuggestionChipButton(suggestion: suggestion, theme: mentionTheme)
664
+ let button = MentionSuggestionChipButton(
665
+ suggestion: suggestion,
666
+ theme: mentionTheme,
667
+ toolbarAppearance: resolvedAppearance
668
+ )
512
669
  button.addTarget(self, action: #selector(handleSelectMentionSuggestion(_:)), for: .touchUpInside)
513
670
  mentionButtons.append(button)
514
671
  mentionStackView.addArrangedSubview(button)
@@ -528,13 +685,45 @@ final class EditorAccessoryToolbarView: UIView {
528
685
  for binding in buttonBindings {
529
686
  let buttonState = buttonState(for: binding.item, state: state)
530
687
  binding.button.isEnabled = buttonState.enabled
688
+ binding.button.isSelected = buttonState.active
531
689
  binding.button.accessibilityTraits = buttonState.active ? [.button, .selected] : .button
532
- updateButtonAppearance(
533
- binding.button,
534
- enabled: buttonState.enabled,
535
- active: buttonState.active
536
- )
690
+ updateButtonAppearance(binding.button, item: binding.item, enabled: buttonState.enabled, active: buttonState.active)
691
+ }
692
+
693
+ #if compiler(>=6.2)
694
+ if #available(iOS 26.0, *), usesNativeBarToolbar {
695
+ for binding in barButtonBindings {
696
+ let state = buttonState(for: binding.item, state: currentState)
697
+ binding.button.isEnabled = state.enabled
698
+ binding.button.isSelected = state.active
699
+ binding.button.style = state.active ? .prominent : .plain
700
+ }
537
701
  }
702
+ #endif
703
+ }
704
+
705
+ var firstButtonAlphaForTesting: CGFloat {
706
+ buttonBindings.first?.button.alpha ?? 0
707
+ }
708
+ var firstButtonTintColorForTesting: UIColor? {
709
+ buttonBindings.first?.button.tintColor
710
+ }
711
+ var firstButtonTintAdjustmentModeForTesting: UIView.TintAdjustmentMode {
712
+ buttonBindings.first?.button.tintAdjustmentMode ?? .automatic
713
+ }
714
+
715
+ func applyBoldStateForTesting(active: Bool, enabled: Bool) {
716
+ apply(
717
+ state: NativeToolbarState(
718
+ marks: ["bold": active],
719
+ nodes: [:],
720
+ commands: [:],
721
+ allowedMarks: enabled ? ["bold"] : [],
722
+ insertableNodes: [],
723
+ canUndo: false,
724
+ canRedo: false
725
+ )
726
+ )
538
727
  }
539
728
 
540
729
  private func setupView() {
@@ -542,8 +731,37 @@ final class EditorAccessoryToolbarView: UIView {
542
731
  chromeView.backgroundColor = .systemBackground
543
732
  chromeView.layer.borderColor = UIColor.separator.cgColor
544
733
  chromeView.layer.borderWidth = 0.5
734
+ chromeView.isOpaque = false
545
735
  addSubview(chromeView)
546
736
 
737
+ blurView.translatesAutoresizingMaskIntoConstraints = false
738
+ blurView.isHidden = true
739
+ blurView.isUserInteractionEnabled = false
740
+ blurView.clipsToBounds = true
741
+ chromeView.addSubview(blurView)
742
+
743
+ glassTintView.translatesAutoresizingMaskIntoConstraints = false
744
+ glassTintView.isHidden = true
745
+ glassTintView.isUserInteractionEnabled = false
746
+ chromeView.addSubview(glassTintView)
747
+
748
+ nativeToolbarScrollView.translatesAutoresizingMaskIntoConstraints = false
749
+ nativeToolbarScrollView.isHidden = true
750
+ nativeToolbarScrollView.backgroundColor = .clear
751
+ nativeToolbarScrollView.showsHorizontalScrollIndicator = false
752
+ nativeToolbarScrollView.showsVerticalScrollIndicator = false
753
+ nativeToolbarScrollView.alwaysBounceHorizontal = true
754
+ nativeToolbarScrollView.alwaysBounceVertical = false
755
+ chromeView.addSubview(nativeToolbarScrollView)
756
+
757
+ nativeToolbarView.translatesAutoresizingMaskIntoConstraints = false
758
+ nativeToolbarView.isHidden = true
759
+ nativeToolbarView.backgroundColor = .clear
760
+ nativeToolbarView.isTranslucent = true
761
+ nativeToolbarView.setContentHuggingPriority(.required, for: .vertical)
762
+ nativeToolbarView.setContentCompressionResistancePriority(.required, for: .vertical)
763
+ nativeToolbarScrollView.addSubview(nativeToolbarView)
764
+
547
765
  contentStackView.translatesAutoresizingMaskIntoConstraints = false
548
766
  contentStackView.axis = .vertical
549
767
  contentStackView.spacing = 0
@@ -589,6 +807,8 @@ final class EditorAccessoryToolbarView: UIView {
589
807
  chromeBottomConstraint = bottom
590
808
  let mentionHeight = mentionScrollView.heightAnchor.constraint(equalToConstant: 0)
591
809
  mentionRowHeightConstraint = mentionHeight
810
+ let nativeToolbarWidth = nativeToolbarView.widthAnchor.constraint(greaterThanOrEqualToConstant: Self.baseHeight)
811
+ nativeToolbarWidthConstraint = nativeToolbarWidth
592
812
 
593
813
  NSLayoutConstraint.activate([
594
814
  chromeView.topAnchor.constraint(equalTo: topAnchor),
@@ -596,6 +816,29 @@ final class EditorAccessoryToolbarView: UIView {
596
816
  trailing,
597
817
  bottom,
598
818
 
819
+ blurView.topAnchor.constraint(equalTo: chromeView.topAnchor),
820
+ blurView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
821
+ blurView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
822
+ blurView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
823
+
824
+ glassTintView.topAnchor.constraint(equalTo: chromeView.topAnchor),
825
+ glassTintView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
826
+ glassTintView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
827
+ glassTintView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
828
+
829
+ nativeToolbarScrollView.topAnchor.constraint(equalTo: chromeView.topAnchor),
830
+ nativeToolbarScrollView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
831
+ nativeToolbarScrollView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
832
+ nativeToolbarScrollView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
833
+
834
+ nativeToolbarView.topAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.topAnchor),
835
+ nativeToolbarView.leadingAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.leadingAnchor),
836
+ nativeToolbarView.trailingAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.trailingAnchor),
837
+ nativeToolbarView.bottomAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.bottomAnchor),
838
+ nativeToolbarView.heightAnchor.constraint(equalTo: nativeToolbarScrollView.frameLayoutGuide.heightAnchor),
839
+ nativeToolbarView.heightAnchor.constraint(greaterThanOrEqualToConstant: Self.baseHeight),
840
+ nativeToolbarWidth,
841
+
599
842
  contentStackView.topAnchor.constraint(equalTo: chromeView.topAnchor, constant: 6),
600
843
  contentStackView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
601
844
  contentStackView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
@@ -621,7 +864,9 @@ final class EditorAccessoryToolbarView: UIView {
621
864
 
622
865
  private func rebuildButtons() {
623
866
  buttonBindings.removeAll()
867
+ barButtonBindings.removeAll()
624
868
  separators.removeAll()
869
+ nativeToolbarDidInitializeScrollPosition = false
625
870
  for arrangedSubview in stackView.arrangedSubviews {
626
871
  stackView.removeArrangedSubview(arrangedSubview)
627
872
  arrangedSubview.removeFromSuperview()
@@ -644,16 +889,137 @@ final class EditorAccessoryToolbarView: UIView {
644
889
  stackView.addArrangedSubview(button)
645
890
  }
646
891
 
892
+ #if compiler(>=6.2)
893
+ if #available(iOS 26.0, *) {
894
+ nativeToolbarView.setItems(makeNativeToolbarItems(from: compactItems), animated: false)
895
+ } else {
896
+ nativeToolbarView.setItems([], animated: false)
897
+ }
898
+ #else
899
+ nativeToolbarView.setItems([], animated: false)
900
+ #endif
901
+
902
+ updateNativeToolbarMetricsIfNeeded()
647
903
  apply(theme: theme)
648
904
  apply(state: currentState)
649
905
  }
650
906
 
907
+ private func updateNativeToolbarMetricsIfNeeded() {
908
+ #if compiler(>=6.2)
909
+ guard #available(iOS 26.0, *), usesNativeBarToolbar else {
910
+ nativeToolbarWidthConstraint?.constant = Self.baseHeight
911
+ nativeToolbarDidInitializeScrollPosition = false
912
+ return
913
+ }
914
+
915
+ let availableWidth = max(chromeView.bounds.width, bounds.width, 1)
916
+ let targetHeight = max(chromeView.bounds.height, Self.baseHeight)
917
+ nativeToolbarView.layoutIfNeeded()
918
+ let fittingSize = nativeToolbarView.sizeThatFits(
919
+ CGSize(width: CGFloat.greatestFiniteMagnitude, height: targetHeight)
920
+ )
921
+ let contentFrames = nativeToolbarView.subviews.compactMap { subview -> CGRect? in
922
+ guard !subview.isHidden,
923
+ subview.alpha > 0.01,
924
+ subview.bounds.width > 0,
925
+ subview.bounds.height > 0
926
+ else {
927
+ return nil
928
+ }
929
+ return subview.frame
930
+ }
931
+ let measuredSubviewWidth: CGFloat
932
+ if let minX = contentFrames.map(\.minX).min(),
933
+ let maxX = contentFrames.map(\.maxX).max()
934
+ {
935
+ measuredSubviewWidth = ceil(maxX + max(0, minX))
936
+ } else {
937
+ measuredSubviewWidth = 0
938
+ }
939
+ let contentWidth = max(ceil(fittingSize.width), measuredSubviewWidth, availableWidth)
940
+ nativeToolbarWidthConstraint?.constant = contentWidth
941
+ nativeToolbarScrollView.alwaysBounceHorizontal = contentWidth > availableWidth
942
+ let minOffsetX = -nativeToolbarScrollView.adjustedContentInset.left
943
+ let maxOffsetX = max(
944
+ minOffsetX,
945
+ contentWidth - nativeToolbarScrollView.bounds.width + nativeToolbarScrollView.adjustedContentInset.right
946
+ )
947
+ let targetOffsetX: CGFloat
948
+ if nativeToolbarDidInitializeScrollPosition {
949
+ targetOffsetX = min(max(nativeToolbarScrollView.contentOffset.x, minOffsetX), maxOffsetX)
950
+ } else {
951
+ targetOffsetX = minOffsetX
952
+ nativeToolbarDidInitializeScrollPosition = true
953
+ }
954
+ if abs(nativeToolbarScrollView.contentOffset.x - targetOffsetX) > 0.5 {
955
+ nativeToolbarScrollView.setContentOffset(
956
+ CGPoint(x: targetOffsetX, y: nativeToolbarScrollView.contentOffset.y),
957
+ animated: false
958
+ )
959
+ }
960
+ #else
961
+ nativeToolbarWidthConstraint?.constant = Self.baseHeight
962
+ nativeToolbarDidInitializeScrollPosition = false
963
+ #endif
964
+ }
965
+
966
+ #if compiler(>=6.2)
967
+ @available(iOS 26.0, *)
968
+ private func makeNativeToolbarItems(from compactItems: [NativeToolbarItem]) -> [UIBarButtonItem] {
969
+ var toolbarItems: [UIBarButtonItem] = []
970
+ var previousWasSeparator = true
971
+
972
+ for item in compactItems {
973
+ if item.type == .separator {
974
+ if !previousWasSeparator, !toolbarItems.isEmpty {
975
+ let spacer = UIBarButtonItem(systemItem: .fixedSpace)
976
+ spacer.width = 0
977
+ toolbarItems.append(spacer)
978
+ }
979
+ previousWasSeparator = true
980
+ continue
981
+ }
982
+
983
+ let state = buttonState(for: item, state: currentState)
984
+ let barButtonItem = makeNativeBarButtonItem(item: item, enabled: state.enabled, active: state.active)
985
+ barButtonBindings.append(BarButtonBinding(item: item, button: barButtonItem))
986
+ toolbarItems.append(barButtonItem)
987
+ previousWasSeparator = false
988
+ }
989
+
990
+ return toolbarItems
991
+ }
992
+
993
+ @available(iOS 26.0, *)
994
+ private func makeNativeBarButtonItem(
995
+ item: NativeToolbarItem,
996
+ enabled: Bool,
997
+ active: Bool
998
+ ) -> UIBarButtonItem {
999
+ let image = item.icon?.resolvedSFSymbolName().flatMap { UIImage(systemName: $0) }
1000
+ let title = image == nil ? item.icon?.resolvedGlyphText() : nil
1001
+ let action = UIAction { [weak self] _ in
1002
+ self?.onPressItem?(item)
1003
+ }
1004
+ let barButtonItem = UIBarButtonItem(title: title, image: image, primaryAction: action, menu: nil)
1005
+
1006
+ barButtonItem.accessibilityLabel = item.label
1007
+ barButtonItem.isEnabled = enabled
1008
+ barButtonItem.isSelected = active
1009
+ barButtonItem.style = active ? .prominent : .plain
1010
+
1011
+ barButtonItem.sharesBackground = true
1012
+ barButtonItem.hidesSharedBackground = active
1013
+ return barButtonItem
1014
+ }
1015
+ #endif
1016
+
651
1017
  private func makeButton(item: NativeToolbarItem) -> UIButton {
652
1018
  let button = UIButton(type: .system)
653
1019
  button.translatesAutoresizingMaskIntoConstraints = false
654
1020
  button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
655
1021
  button.accessibilityLabel = item.label
656
- button.layer.cornerRadius = theme?.buttonBorderRadius ?? 8
1022
+ button.layer.cornerRadius = resolvedButtonBorderRadius
657
1023
  button.clipsToBounds = true
658
1024
  if #available(iOS 15.0, *) {
659
1025
  var configuration = UIButton.Configuration.plain()
@@ -685,7 +1051,7 @@ final class EditorAccessoryToolbarView: UIView {
685
1051
  button.addAction(UIAction { [weak self] _ in
686
1052
  self?.onPressItem?(item)
687
1053
  }, for: .touchUpInside)
688
- updateButtonAppearance(button, enabled: true, active: false)
1054
+ updateButtonAppearance(button, item: item, enabled: true, active: false)
689
1055
  return button
690
1056
  }
691
1057
 
@@ -712,6 +1078,11 @@ final class EditorAccessoryToolbarView: UIView {
712
1078
  enabled: state.allowedMarks.contains(mark),
713
1079
  active: state.marks[mark] == true
714
1080
  )
1081
+ case .blockquote:
1082
+ return (
1083
+ enabled: state.commands["toggleBlockquote"] == true,
1084
+ active: state.nodes["blockquote"] == true
1085
+ )
715
1086
  case .list:
716
1087
  switch item.listType {
717
1088
  case .bulletList:
@@ -762,7 +1133,54 @@ final class EditorAccessoryToolbarView: UIView {
762
1133
  }
763
1134
  }
764
1135
 
765
- private func updateButtonAppearance(_ button: UIButton, enabled: Bool, active: Bool) {
1136
+ private func updateButtonAppearance(
1137
+ _ button: UIButton,
1138
+ item: NativeToolbarItem,
1139
+ enabled: Bool,
1140
+ active: Bool
1141
+ ) {
1142
+ #if compiler(>=6.2)
1143
+ if #available(iOS 26.0, *), usesFloatingGlassButtons {
1144
+ var configuration = active
1145
+ ? UIButton.Configuration.prominentGlass()
1146
+ : UIButton.Configuration.glass()
1147
+ configuration.cornerStyle = .capsule
1148
+ configuration.contentInsets = NSDirectionalEdgeInsets(
1149
+ top: 8,
1150
+ leading: 10,
1151
+ bottom: 8,
1152
+ trailing: 10
1153
+ )
1154
+ configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(
1155
+ pointSize: 16,
1156
+ weight: .semibold
1157
+ )
1158
+ if let symbolName = item.icon?.resolvedSFSymbolName(),
1159
+ let symbolImage = UIImage(systemName: symbolName)
1160
+ {
1161
+ configuration.image = symbolImage
1162
+ configuration.title = nil
1163
+ } else {
1164
+ configuration.image = nil
1165
+ configuration.title = item.icon?.resolvedGlyphText() ?? "?"
1166
+ }
1167
+ button.configuration = configuration
1168
+ button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
1169
+ button.alpha = enabled ? 1 : 0.45
1170
+ return
1171
+ }
1172
+ #endif
1173
+
1174
+ if resolvedAppearance == .native {
1175
+ button.tintColor = enabled ? nil : .systemGray
1176
+ button.tintAdjustmentMode = enabled ? .automatic : .normal
1177
+ button.alpha = 1
1178
+ button.backgroundColor = active
1179
+ ? UIColor.white.withAlphaComponent(0.18)
1180
+ : .clear
1181
+ return
1182
+ }
1183
+
766
1184
  let tintColor: UIColor
767
1185
  if !enabled {
768
1186
  tintColor = theme?.buttonDisabledColor ?? .tertiaryLabel
@@ -774,11 +1192,107 @@ final class EditorAccessoryToolbarView: UIView {
774
1192
 
775
1193
  button.tintColor = tintColor
776
1194
  button.setTitleColor(tintColor, for: .normal)
1195
+ button.alpha = enabled ? 1 : 0.7
777
1196
  button.backgroundColor = active
778
1197
  ? (theme?.buttonActiveBackgroundColor ?? UIColor.systemBlue.withAlphaComponent(0.12))
779
1198
  : .clear
780
1199
  }
781
1200
 
1201
+ private var resolvedAppearance: EditorToolbarAppearance {
1202
+ theme?.appearance ?? .custom
1203
+ }
1204
+
1205
+ private var resolvedHorizontalInset: CGFloat {
1206
+ theme?.resolvedHorizontalInset ?? Self.defaultHorizontalInset
1207
+ }
1208
+
1209
+ private var resolvedKeyboardOffset: CGFloat {
1210
+ theme?.resolvedKeyboardOffset ?? Self.defaultKeyboardOffset
1211
+ }
1212
+
1213
+ private var resolvedBorderRadius: CGFloat {
1214
+ theme?.resolvedBorderRadius ?? 0
1215
+ }
1216
+
1217
+ private var resolvedBorderWidth: CGFloat {
1218
+ theme?.resolvedBorderWidth ?? 0.5
1219
+ }
1220
+
1221
+ private var resolvedButtonBorderRadius: CGFloat {
1222
+ theme?.resolvedButtonBorderRadius ?? 8
1223
+ }
1224
+
1225
+ private var usesFloatingGlassButtons: Bool {
1226
+ return false
1227
+ }
1228
+
1229
+ private var usesNativeBarToolbar: Bool {
1230
+ return false
1231
+ }
1232
+
1233
+ private var activeNativeToolbarScrollViewForTesting: UIScrollView {
1234
+ usesNativeBarToolbar ? nativeToolbarScrollView : scrollView
1235
+ }
1236
+
1237
+ private func resolvedBlurEffect() -> UIVisualEffect {
1238
+ #if compiler(>=6.2)
1239
+ if #available(iOS 26.0, *) {
1240
+ let effect = UIGlassEffect(style: .regular)
1241
+ effect.isInteractive = true
1242
+ effect.tintColor = resolvedGlassEffectTintColor
1243
+ return effect
1244
+ }
1245
+ #endif
1246
+ if #available(iOS 13.0, *) {
1247
+ return UIBlurEffect(style: .systemUltraThinMaterial)
1248
+ }
1249
+ return UIBlurEffect(style: .extraLight)
1250
+ }
1251
+
1252
+ private var resolvedEffectAlpha: CGFloat {
1253
+ if #available(iOS 26.0, *), resolvedAppearance == .native {
1254
+ return 1
1255
+ }
1256
+ return resolvedAppearance == .native ? 0.72 : 1
1257
+ }
1258
+
1259
+ private var resolvedGlassTintAlpha: CGFloat {
1260
+ if #available(iOS 26.0, *), resolvedAppearance == .native {
1261
+ return 0
1262
+ }
1263
+ return resolvedAppearance == .native ? 0.12 : 0
1264
+ }
1265
+
1266
+ private var resolvedGlassEffectTintColor: UIColor {
1267
+ return .clear
1268
+ }
1269
+
1270
+ private var resolvedBorderColor: UIColor {
1271
+ if resolvedAppearance != .native {
1272
+ return theme?.borderColor ?? UIColor.separator
1273
+ }
1274
+ if #available(iOS 26.0, *) {
1275
+ return .clear
1276
+ }
1277
+ return UIColor.separator.withAlphaComponent(0.22)
1278
+ }
1279
+
1280
+ private func refreshNativeHostTransparencyIfNeeded() {
1281
+ guard usesFloatingGlassButtons else { return }
1282
+ backgroundColor = .clear
1283
+ isOpaque = false
1284
+
1285
+ var ancestor: UIView? = self
1286
+ while let view = ancestor {
1287
+ let className = NSStringFromClass(type(of: view))
1288
+ if view === self || className.contains("UIInput") || className.contains("Accessory") {
1289
+ view.backgroundColor = .clear
1290
+ view.isOpaque = false
1291
+ }
1292
+ ancestor = view.superview
1293
+ }
1294
+ }
1295
+
782
1296
  @objc private func handleSelectMentionSuggestion(_ sender: MentionSuggestionChipButton) {
783
1297
  onSelectMentionSuggestion?(sender.suggestion)
784
1298
  }
@@ -791,18 +1305,17 @@ final class EditorAccessoryToolbarView: UIView {
791
1305
 
792
1306
  class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognizerDelegate {
793
1307
 
794
- private static let updateLog = Logger(
795
- subsystem: "com.apollohg.prose-editor",
796
- category: "view-command"
797
- )
798
-
799
1308
  // MARK: - Subviews
800
1309
 
801
1310
  let richTextView: RichTextEditorView
802
- private let accessoryToolbar = EditorAccessoryToolbarView()
1311
+ private let accessoryToolbar = EditorAccessoryToolbarView(
1312
+ frame: .zero,
1313
+ inputViewStyle: .keyboard
1314
+ )
803
1315
  private var toolbarFrameInWindow: CGRect?
804
1316
  private var didApplyAutoFocus = false
805
1317
  private var toolbarState = NativeToolbarState.empty
1318
+ private var toolbarItems: [NativeToolbarItem] = NativeToolbarItem.defaults
806
1319
  private var showsToolbar = true
807
1320
  private var toolbarPlacement = "keyboard"
808
1321
  private var heightBehavior: EditorHeightBehavior = .fixed
@@ -920,6 +1433,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
920
1433
  toolbarState = .empty
921
1434
  accessoryToolbar.apply(state: .empty)
922
1435
  }
1436
+ refreshSystemAssistantToolbarIfNeeded()
923
1437
  refreshMentionQuery()
924
1438
  }
925
1439
 
@@ -928,8 +1442,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
928
1442
  richTextView.applyTheme(theme)
929
1443
  accessoryToolbar.apply(theme: theme?.toolbar)
930
1444
  accessoryToolbar.apply(mentionTheme: theme?.mentions ?? addons.mentions?.theme)
1445
+ refreshSystemAssistantToolbarIfNeeded()
931
1446
  if richTextView.textView.isFirstResponder,
932
- richTextView.textView.inputAccessoryView === accessoryToolbar
1447
+ (richTextView.textView.inputAccessoryView === accessoryToolbar || shouldUseSystemAssistantToolbar)
933
1448
  {
934
1449
  richTextView.textView.reloadInputViews()
935
1450
  }
@@ -941,6 +1456,10 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
941
1456
  refreshMentionQuery()
942
1457
  }
943
1458
 
1459
+ func setRemoteSelectionsJson(_ remoteSelectionsJson: String?) {
1460
+ richTextView.setRemoteSelections(RemoteSelectionDecoration.from(json: remoteSelectionsJson))
1461
+ }
1462
+
944
1463
  func setEditable(_ editable: Bool) {
945
1464
  richTextView.textView.isEditable = editable
946
1465
  updateAccessoryToolbarVisibility()
@@ -974,6 +1493,10 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
974
1493
  }
975
1494
  }
976
1495
 
1496
+ func setAllowImageResizing(_ allowImageResizing: Bool) {
1497
+ richTextView.allowImageResizing = allowImageResizing
1498
+ }
1499
+
977
1500
  private func emitContentHeightIfNeeded(force: Bool = false) {
978
1501
  guard heightBehavior == .autoGrow else { return }
979
1502
  let contentHeight = ceil(richTextView.intrinsicContentSize.height)
@@ -984,7 +1507,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
984
1507
  }
985
1508
 
986
1509
  func setToolbarButtonsJson(_ toolbarButtonsJson: String?) {
987
- accessoryToolbar.setItems(NativeToolbarItem.from(json: toolbarButtonsJson))
1510
+ toolbarItems = NativeToolbarItem.from(json: toolbarButtonsJson)
1511
+ accessoryToolbar.setItems(toolbarItems)
1512
+ refreshSystemAssistantToolbarIfNeeded()
988
1513
  }
989
1514
 
990
1515
  func setToolbarFrameJson(_ toolbarFrameJson: String?) {
@@ -1024,13 +1549,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1024
1549
  /// Apply an editor update from JS. Sets the echo-suppression flag so the
1025
1550
  /// resulting delegate callback is NOT re-dispatched back to JS.
1026
1551
  func applyEditorUpdate(_ updateJson: String) {
1027
- Self.updateLog.debug("[applyEditorUpdate.begin] bytes=\(updateJson.utf8.count)")
1028
1552
  isApplyingJSUpdate = true
1029
1553
  richTextView.textView.applyUpdateJSON(updateJson)
1030
1554
  isApplyingJSUpdate = false
1031
- Self.updateLog.debug(
1032
- "[applyEditorUpdate.end] textState=\(self.richTextView.textView.textStorage.string.count)"
1033
- )
1034
1555
  }
1035
1556
 
1036
1557
  // MARK: - Focus Commands
@@ -1047,12 +1568,14 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1047
1568
 
1048
1569
  @objc private func textViewDidBeginEditing(_ notification: Notification) {
1049
1570
  installOutsideTapRecognizerIfNeeded()
1571
+ richTextView.textView.refreshSelectionVisualState()
1050
1572
  refreshMentionQuery()
1051
1573
  onFocusChange(["isFocused": true])
1052
1574
  }
1053
1575
 
1054
1576
  @objc private func textViewDidEndEditing(_ notification: Notification) {
1055
1577
  uninstallOutsideTapRecognizer()
1578
+ richTextView.textView.refreshSelectionVisualState()
1056
1579
  clearMentionQueryStateAndHidePopover()
1057
1580
  onFocusChange(["isFocused": false])
1058
1581
  }
@@ -1060,6 +1583,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1060
1583
  @objc private func handleOutsideTap(_ recognizer: UITapGestureRecognizer) {
1061
1584
  guard recognizer.state == .ended else { return }
1062
1585
  guard richTextView.textView.isFirstResponder else { return }
1586
+ guard let tapWindow = gestureWindow ?? window else { return }
1587
+ let locationInWindow = recognizer.location(in: tapWindow)
1588
+ guard shouldHandleOutsideTap(locationInWindow: locationInWindow, touchedView: nil) else {
1589
+ return
1590
+ }
1063
1591
  blur()
1064
1592
  }
1065
1593
 
@@ -1082,16 +1610,32 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1082
1610
 
1083
1611
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
1084
1612
  guard gestureRecognizer === outsideTapGestureRecognizer else { return true }
1085
- if let touchedView = touch.view, touchedView.isDescendant(of: self) {
1613
+ guard let tapWindow = gestureWindow ?? window else { return true }
1614
+ let locationInWindow = touch.location(in: tapWindow)
1615
+ let result = shouldHandleOutsideTap(
1616
+ locationInWindow: locationInWindow,
1617
+ touchedView: touch.view
1618
+ )
1619
+ return result
1620
+ }
1621
+
1622
+ private func shouldHandleOutsideTap(
1623
+ locationInWindow: CGPoint,
1624
+ touchedView: UIView?
1625
+ ) -> Bool {
1626
+ if let touchedView, touchedView.isDescendant(of: self) {
1086
1627
  return false
1087
1628
  }
1088
- if let touchedView = touch.view, touchedView.isDescendant(of: accessoryToolbar) {
1629
+ if let tapWindow = gestureWindow ?? window {
1630
+ let editorFrameInWindow = convert(bounds, to: tapWindow)
1631
+ if editorFrameInWindow.contains(locationInWindow) {
1632
+ return false
1633
+ }
1634
+ }
1635
+ if let touchedView, touchedView.isDescendant(of: accessoryToolbar) {
1089
1636
  return false
1090
1637
  }
1091
- if let toolbarFrameInWindow,
1092
- let window = gestureWindow,
1093
- toolbarFrameInWindow.contains(touch.location(in: window))
1094
- {
1638
+ if let toolbarFrameInWindow, toolbarFrameInWindow.contains(locationInWindow) {
1095
1639
  return false
1096
1640
  }
1097
1641
  return true
@@ -1101,7 +1645,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1101
1645
 
1102
1646
  func editorTextView(_ textView: EditorTextView, selectionDidChange anchor: UInt32, head: UInt32) {
1103
1647
  refreshToolbarStateFromEditorSelection()
1648
+ refreshSystemAssistantToolbarIfNeeded()
1104
1649
  refreshMentionQuery()
1650
+ richTextView.refreshRemoteSelections()
1105
1651
  onSelectionChange(["anchor": Int(anchor), "head": Int(head)])
1106
1652
  }
1107
1653
 
@@ -1109,10 +1655,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1109
1655
  if let state = NativeToolbarState(updateJSON: updateJSON) {
1110
1656
  toolbarState = state
1111
1657
  accessoryToolbar.apply(state: state)
1658
+ refreshSystemAssistantToolbarIfNeeded()
1112
1659
  }
1113
1660
  refreshMentionQuery()
1661
+ richTextView.refreshRemoteSelections()
1114
1662
  guard !isApplyingJSUpdate else { return }
1115
- Self.updateLog.debug("[didReceiveUpdate] bytes=\(updateJSON.utf8.count)")
1116
1663
  onEditorUpdate(["updateJson": updateJSON])
1117
1664
  }
1118
1665
 
@@ -1131,6 +1678,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1131
1678
  accessoryToolbar.onSelectMentionSuggestion = { [weak self] suggestion in
1132
1679
  self?.insertMentionSuggestion(suggestion)
1133
1680
  }
1681
+ accessoryToolbar.setItems(toolbarItems)
1134
1682
  accessoryToolbar.apply(state: toolbarState)
1135
1683
  updateAccessoryToolbarVisibility()
1136
1684
  }
@@ -1154,6 +1702,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1154
1702
  mentionQueryState = queryState
1155
1703
  accessoryToolbar.apply(mentionTheme: richTextView.textView.theme?.mentions ?? mentions.theme)
1156
1704
  let didChangeToolbarHeight = accessoryToolbar.setMentionSuggestions(suggestions)
1705
+ refreshSystemAssistantToolbarIfNeeded()
1157
1706
  if didChangeToolbarHeight,
1158
1707
  richTextView.textView.isFirstResponder,
1159
1708
  richTextView.textView.inputAccessoryView === accessoryToolbar
@@ -1172,6 +1721,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1172
1721
  private func clearMentionQueryStateAndHidePopover() {
1173
1722
  mentionQueryState = nil
1174
1723
  let didChangeToolbarHeight = accessoryToolbar.setMentionSuggestions([])
1724
+ refreshSystemAssistantToolbarIfNeeded()
1175
1725
  if didChangeToolbarHeight,
1176
1726
  richTextView.textView.isFirstResponder,
1177
1727
  richTextView.textView.inputAccessoryView === accessoryToolbar
@@ -1365,9 +1915,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1365
1915
  accessoryToolbar.triggerMentionSuggestionTapForTesting(at: index)
1366
1916
  }
1367
1917
  private func updateAccessoryToolbarVisibility() {
1918
+ refreshSystemAssistantToolbarIfNeeded()
1368
1919
  let nextAccessoryView: UIView? = showsToolbar &&
1369
1920
  toolbarPlacement == "keyboard" &&
1370
- richTextView.textView.isEditable
1921
+ richTextView.textView.isEditable &&
1922
+ !shouldUseSystemAssistantToolbar
1371
1923
  ? accessoryToolbar
1372
1924
  : nil
1373
1925
  if richTextView.textView.inputAccessoryView !== nextAccessoryView {
@@ -1378,6 +1930,19 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1378
1930
  }
1379
1931
  }
1380
1932
 
1933
+ private var shouldUseSystemAssistantToolbar: Bool {
1934
+ false
1935
+ }
1936
+
1937
+ private func refreshSystemAssistantToolbarIfNeeded() {
1938
+ guard #available(iOS 26.0, *) else { return }
1939
+
1940
+ let assistantItem = richTextView.textView.inputAssistantItem
1941
+ assistantItem.allowsHidingShortcuts = false
1942
+ assistantItem.leadingBarButtonGroups = []
1943
+ assistantItem.trailingBarButtonGroups = []
1944
+ }
1945
+
1381
1946
  private func handleListToggle(_ listType: String) {
1382
1947
  let isActive = toolbarState.nodes[listType] == true
1383
1948
  richTextView.textView.performToolbarToggleList(listType, isActive: isActive)
@@ -1388,6 +1953,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1388
1953
  case .mark:
1389
1954
  guard let mark = item.mark else { return }
1390
1955
  richTextView.textView.performToolbarToggleMark(mark)
1956
+ case .blockquote:
1957
+ richTextView.textView.performToolbarToggleBlockquote()
1391
1958
  case .list:
1392
1959
  guard let listType = item.listType?.rawValue else { return }
1393
1960
  handleListToggle(listType)