Explorar el Código

enhance(ios): native editor commands toolbar

Tienson Qin hace 2 meses
padre
commit
be09bf3c86

+ 4 - 0
ios/App/App.xcodeproj/project.pbxproj

@@ -42,6 +42,7 @@
 		D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
 		D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */; };
+		D3F4A5B62F1234567890ABD4 /* NativeEditorToolbarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */; };
 		D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */; };
 		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
@@ -124,6 +125,7 @@
 		D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
 		D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
 		D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeBottomSheetPlugin.swift; sourceTree = "<group>"; };
+		D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeEditorToolbarPlugin.swift; sourceTree = "<group>"; };
 		D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSelectionActionBarPlugin.swift; sourceTree = "<group>"; };
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
 		FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
@@ -213,6 +215,7 @@
 				CBF2D2D92DE83CB0006338BE /* UILocalPlugin.swift */,
 				ABCDEF0123456789000000AA /* NativeTopBarPlugin.swift */,
 				D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */,
+				D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */,
 				D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */,
 				5FF86329283B5ADB0047731B /* Utils.swift */,
 				5FF8632B283B5BFD0047731B /* Utils.m */,
@@ -466,6 +469,7 @@
 				CBF2D2DA2DE83CB8006338BE /* UILocalPlugin.swift in Sources */,
 				ABCDEF0123456789000000AB /* NativeTopBarPlugin.swift in Sources */,
 				D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */,
+				D3F4A5B62F1234567890ABD4 /* NativeEditorToolbarPlugin.swift in Sources */,
 				D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */,
 				A1B2C3D41E2F3A4B5C6D7E90 /* NativePageViewController.swift in Sources */,
 				D3C620AA2ED4B9A80009CCDA /* Theme.swift in Sources */,

+ 1 - 0
ios/App/App/AppViewController.swift

@@ -15,6 +15,7 @@ import UIKit
         bridge?.registerPluginInstance(NativeTopBarPlugin())
         bridge?.registerPluginInstance(LiquidTabsPlugin())
         bridge?.registerPluginInstance(NativeBottomSheetPlugin())
+        bridge?.registerPluginInstance(NativeEditorToolbarPlugin())
         bridge?.registerPluginInstance(NativeSelectionActionBarPlugin())
     }
 

+ 3 - 3
ios/App/App/Assets.xcassets/Contents.json

@@ -1,6 +1,6 @@
 {
   "info" : {
-    "version" : 1,
-    "author" : "xcode"
+    "author" : "xcode",
+    "version" : 1
   }
-}
+}

+ 12 - 0
ios/App/App/Assets.xcassets/brackets.symbolset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "symbols" : [
+    {
+      "filename" : "brackets.symbols.svg",
+      "idiom" : "universal"
+    }
+  ]
+}

+ 103 - 0
ios/App/App/Assets.xcassets/brackets.symbolset/brackets.symbols.svg

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generator: Apple Native CoreSVG 341-->
+<!DOCTYPE svg
+PUBLIC "-//W3C//DTD SVG 1.1//EN"
+       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
+ <!--glyph: "", point size: 100.0, font version: "21.0d6e2", template writer version: "138.0.0"-->
+ <style>.defaults {-sfsymbols-variable-value-mode:color;-sfsymbols-draw-reverses-motion-groups:true}
+
+.monochrome-0 {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
+
+.multicolor-0:tintColor {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
+
+.hierarchical-0:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
+
+.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
+</style>
+ <g id="Notes">
+  <rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
+  <line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
+  <line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
+  <g transform="matrix(0.2 0 0 0.2 263 1933)">
+   <path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm3.61328-17.7734v-28.4668c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v28.4668c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094Zm-17.8223-10.5957h28.418c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-28.418c-2.24609 0-3.75977 1.51367-3.75977 3.71094 0 2.14844 1.51367 3.61328 3.75977 3.61328Z"/>
+  </g>
+  <g transform="matrix(0.2 0 0 0.2 281.506 1933)">
+   <path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm4.05273-23.0957v-36.9141c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v36.9141c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039Zm-22.5586-14.4043h36.9629c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-36.9629c-2.49023 0-4.15039 1.70898-4.15039 4.15039 0 2.39258 1.66016 4.00391 4.15039 4.00391Z"/>
+  </g>
+  <g transform="matrix(0.2 0 0 0.2 304.924 1933)">
+   <path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm4.44336-30.3223v-48.4863c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v48.4863c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984Zm-28.7109-19.7754h48.4863c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-48.4863c-2.73438 0-4.58984 1.85547-4.58984 4.58984 0 2.58789 1.85547 4.39453 4.58984 4.39453Z"/>
+  </g>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
+  <line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
+  <g transform="matrix(0.2 0 0 0.2 776 1933)">
+   <path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l20.5566-57.5195h0.244141l20.6055 57.5195c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm10.2051-20.9473h30.6641c2.00195 0 3.66211-1.66016 3.66211-3.66211 0-2.05078-1.66016-3.66211-3.66211-3.66211h-30.6641c-2.00195 0-3.66211 1.61133-3.66211 3.66211 0 2.00195 1.66016 3.66211 3.66211 3.66211Z"/>
+  </g>
+  <line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
+  <g transform="matrix(0.2 0 0 0.2 1289 1933)">
+   <path d="m14.209 13.1348 7.86133 7.86133c4.29688 4.39453 9.32617 4.10156 13.8672-1.02539l60.6934-68.2129-4.88281-4.88281-60.2539 67.6758c-1.80664 1.95312-3.4668 2.44141-5.81055 0.0976562l-5.17578-5.12695c-2.29492-2.29492-1.80664-3.95508 0.195312-5.81055l67.4805-62.1582-4.88281-4.83398-68.0664 62.5977c-4.98047 4.58984-5.32227 9.47266-1.02539 13.8184Zm44.873-97.4609c-2.05078 2.00195-2.24609 4.88281-1.07422 6.78711 1.12305 1.80664 3.4668 3.02734 6.5918 2.24609 5.85938-1.66016 12.5977-2.39258 18.8965 0.927734l-2.68555 7.12891c-1.61133 4.00391-0.732422 6.88477 1.70898 9.42383l10.2539 10.3027c2.34375 2.39258 4.54102 2.44141 7.08008 1.95312l4.44336-0.732422 2.58789 2.53906-0.195312 2.24609c-0.0976562 2.29492 0.537109 4.29688 2.7832 6.49414l3.36914 3.32031c2.29492 2.29492 5.51758 2.49023 7.8125 0.195312l12.9883-13.0371c2.29492-2.34375 2.14844-5.37109-0.195312-7.66602l-3.41797-3.41797c-2.19727-2.19727-4.05273-3.02734-6.34766-2.88086l-2.34375 0.244141-2.44141-2.44141 1.02539-4.6875c0.634766-2.73438-0.244141-4.98047-2.88086-7.61719l-11.2793-11.1816c-12.9395-12.8418-35.5957-11.0352-46.6797-0.146484Zm7.08008 2.05078c8.78906-6.39648 25.9766-5.66406 33.6914 1.95312l12.3047 12.207c1.02539 1.02539 1.2207 1.80664 0.927734 3.32031l-1.46484 6.64062 6.73828 6.68945 4.39453-0.244141c1.12305-0.0488281 1.51367 0.0488281 2.34375 0.878906l2.53906 2.49023-10.8398 10.8398-2.49023-2.49023c-0.830078-0.878906-0.976562-1.2207-0.927734-2.39258l0.292969-4.3457-6.68945-6.73828-6.83594 1.17188c-1.41602 0.292969-2.05078 0.195312-3.17383-0.878906l-8.93555-8.88672c-1.07422-1.02539-1.17188-1.70898-0.488281-3.36914l4.58984-11.4746c-6.10352-6.34766-17.041-7.51953-25.5859-4.58984-0.683594 0.244141-0.927734-0.390625-0.390625-0.78125Z"/>
+  </g>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
+  <text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.7.0</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 17 or greater</text>
+  <text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from brackets.symbols</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
+  <text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
+ </g>
+ <g id="Guides">
+  <g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
+   <path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
+  </g>
+  <line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
+  <line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
+  <g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
+   <path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
+  </g>
+  <line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
+  <line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
+  <g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
+   <path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
+  </g>
+  <line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
+  <line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
+  <line id="right-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2977.69" x2="2977.69" y1="600.785" y2="720.121"/>
+  <line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2889.11" x2="2889.11" y1="600.785" y2="720.121"/>
+  <line id="right-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1494.13" x2="1494.13" y1="600.785" y2="720.121"/>
+  <line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1405.56" x2="1405.56" y1="600.785" y2="720.121"/>
+  <line id="right-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="604" x2="604" y1="600.785" y2="720.121"/>
+  <line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="515.423" x2="515.423" y1="600.785" y2="720.121"/>
+ </g>
+ <g id="Symbols">
+  <g id="Black-S" transform="matrix(1.00656 0 0 1.00656 2889.11 696)">
+   <path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
+  </g>
+  <g id="Regular-S" transform="matrix(1.00656 0 0 1.00656 1405.56 696)">
+   <path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
+  </g>
+  <g id="Ultralight-S" transform="matrix(1.00656 0 0 1.00656 515.423 696)">
+   <path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
+  </g>
+ </g>
+</svg>

+ 457 - 0
ios/App/App/NativeEditorToolbarPlugin.swift

@@ -0,0 +1,457 @@
+import Capacitor
+import UIKit
+
+private struct NativeEditorAction {
+    let id: String
+    let title: String
+    let systemIcon: String?
+
+    init?(jsObject: JSObject) {
+        guard let id = jsObject["id"] as? String else { return nil }
+        self.id = id
+        self.title = (jsObject["title"] as? String) ?? id
+        self.systemIcon = jsObject["systemIcon"] as? String
+    }
+}
+
+private class NativeEditorToolbarView: UIView {
+    /// Callback when any action is tapped.
+    var onActionTapped: ((String) -> Void)?
+
+    /// Used to prevent an old dismiss animation from removing a newly-presented bar.
+    private var dismissGeneration: Int = 0
+
+    private let blurView: UIVisualEffectView = {
+        let effect = UIBlurEffect(style: .systemChromeMaterial)
+        let view = UIVisualEffectView(effect: effect)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.layer.cornerRadius = 18
+        view.clipsToBounds = true
+        view.isUserInteractionEnabled = true
+        return view
+    }()
+
+    private let rootStack: UIStackView = {
+        let stack = UIStackView()
+        stack.axis = .horizontal
+        stack.alignment = .center
+        stack.spacing = 6
+        stack.isLayoutMarginsRelativeArrangement = true
+        stack.layoutMargins = UIEdgeInsets(top: 7, left: 10, bottom: 7, right: 10)
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        return stack
+    }()
+
+    private let actionsScrollView: UIScrollView = {
+        let view = UIScrollView()
+        view.showsHorizontalScrollIndicator = false
+        view.showsVerticalScrollIndicator = false
+        view.alwaysBounceHorizontal = true
+        view.contentInsetAdjustmentBehavior = .never
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    private let actionsStack: UIStackView = {
+        let stack = UIStackView()
+        stack.axis = .horizontal
+        stack.alignment = .center
+        stack.spacing = 4
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.isLayoutMarginsRelativeArrangement = true
+        stack.layoutMargins = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2)
+        return stack
+    }()
+
+    private let trailingContainer: UIStackView = {
+        let stack = UIStackView()
+        stack.axis = .horizontal
+        stack.alignment = .center
+        stack.spacing = 6
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.isUserInteractionEnabled = true
+        return stack
+    }()
+
+    private let separator: UIView = {
+        let view = UIView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.backgroundColor = UIColor.label.withAlphaComponent(0.08)
+        view.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
+        view.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
+        return view
+    }()
+
+    private let trailingButton: UIButton = {
+        let button = UIButton(type: .system)
+        button.translatesAutoresizingMaskIntoConstraints = false
+        button.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
+        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
+        return button
+    }()
+
+    private var trailingActionId: String?
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupView()
+    }
+
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupView()
+    }
+
+    // MARK: - Public
+
+    func present(on host: UIView,
+                 actions: [NativeEditorAction],
+                 trailingAction: NativeEditorAction?,
+                 tintColor: UIColor?,
+                 backgroundColor: UIColor?) {
+        // Bump generation to invalidate any previous dismiss completion
+        dismissGeneration += 1
+        layer.removeAllAnimations()
+
+        // We ignore tintColor/backgroundColor – they’re now driven by theme.
+        configure(actions: actions,
+                  trailingAction: trailingAction)
+        attachIfNeeded(to: host)
+        animateIn()
+    }
+
+    func dismiss(animated: Bool = true) {
+        dismissGeneration += 1
+        let currentGen = dismissGeneration
+
+        layer.removeAllAnimations()
+
+        guard animated else {
+            removeFromSuperview()
+            transform = .identity
+            alpha = 1
+            return
+        }
+
+        UIView.animate(withDuration: 0.16,
+                       delay: 0,
+                       options: [.curveEaseIn, .allowUserInteraction],
+                       animations: {
+            self.alpha = 0
+            self.transform = CGAffineTransform(translationX: 0, y: 8)
+        }, completion: { _ in
+            // Only remove if no newer present/dismiss has happened.
+            if currentGen == self.dismissGeneration {
+                self.removeFromSuperview()
+                self.transform = .identity
+                self.alpha = 1
+            }
+        })
+    }
+
+    // MARK: - Private helpers
+
+    private func setupView() {
+        backgroundColor = .clear
+        isOpaque = false
+
+        layer.cornerRadius = 18
+        layer.masksToBounds = false
+        layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor
+        layer.shadowOpacity = 0.25
+        layer.shadowOffset = CGSize(width: 0, height: 8)
+        layer.shadowRadius = 22
+
+        layer.borderColor = UIColor.label.withAlphaComponent(0.04).cgColor
+        layer.borderWidth = 0.5
+
+        addSubview(blurView)
+        NSLayoutConstraint.activate([
+            blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
+            blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
+            blurView.topAnchor.constraint(equalTo: topAnchor),
+            blurView.bottomAnchor.constraint(equalTo: bottomAnchor)
+        ])
+
+        blurView.contentView.addSubview(rootStack)
+        NSLayoutConstraint.activate([
+            rootStack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
+            rootStack.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
+            rootStack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
+            rootStack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor)
+        ])
+
+        actionsScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+        actionsScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
+        rootStack.addArrangedSubview(actionsScrollView)
+
+        trailingContainer.setContentHuggingPriority(.required, for: .horizontal)
+        trailingContainer.setContentCompressionResistancePriority(.required, for: .horizontal)
+        rootStack.addArrangedSubview(trailingContainer)
+
+        actionsScrollView.addSubview(actionsStack)
+        NSLayoutConstraint.activate([
+            actionsStack.leadingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.leadingAnchor),
+            actionsStack.trailingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.trailingAnchor),
+            actionsStack.topAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.topAnchor),
+            actionsStack.bottomAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.bottomAnchor),
+            actionsStack.heightAnchor.constraint(equalTo: actionsScrollView.frameLayoutGuide.heightAnchor)
+        ])
+
+        trailingContainer.isHidden = true
+        trailingContainer.addArrangedSubview(separator)
+        trailingContainer.addArrangedSubview(trailingButton)
+
+        trailingButton.addTarget(self, action: #selector(handleTrailingTap(_:)), for: .touchUpInside)
+    }
+
+    /// Returns the theme-appropriate tint (light: black, dark: white).
+    private func currentTintColor() -> UIColor {
+        if #available(iOS 12.0, *) {
+            return traitCollection.userInterfaceStyle == .dark ? .white : .black
+        } else {
+            return .black
+        }
+    }
+
+    private func configure(actions: [NativeEditorAction],
+                           trailingAction: NativeEditorAction?) {
+        let tint = currentTintColor()
+        // Always use Logseq background (theme-aware)
+        let bgBase = UIColor.logseqBackground
+
+        blurView.backgroundColor = bgBase.withAlphaComponent(0.9)
+        blurView.contentView.backgroundColor = .clear
+
+        // Main actions
+        actionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
+        actions.forEach { action in
+            let button = makeButton(for: action, tintColor: tint)
+            actionsStack.addArrangedSubview(button)
+        }
+
+        // Trailing action (keyboard hide or audio)
+        if let trailingAction {
+            trailingContainer.isHidden = false
+            trailingActionId = trailingAction.id
+            separator.isHidden = false
+
+            var config = UIButton.Configuration.plain()
+            config.baseForegroundColor = tint
+            config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
+            config.preferredSymbolConfigurationForImage =
+                UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
+            config.background = .clear()
+
+            let trailingSymbol = trailingAction.systemIcon ?? "keyboard.chevron.compact.down"
+            config.image = UIImage(systemName: trailingSymbol) ?? UIImage(systemName: "keyboard.chevron.compact.down")
+
+            trailingButton.configuration = config
+            trailingButton.tintColor = tint
+            trailingButton.accessibilityIdentifier = trailingAction.id
+
+            trailingButton.configurationUpdateHandler = { button in
+                var cfg = button.configuration
+                if button.isHighlighted {
+                    cfg?.background.backgroundColor = tint.withAlphaComponent(0.18)
+                } else {
+                    cfg?.background.backgroundColor = UIColor.clear
+                }
+                button.configuration = cfg
+            }
+        } else {
+            trailingContainer.isHidden = true
+            trailingActionId = nil
+            separator.isHidden = true
+            trailingButton.configuration = nil
+            trailingButton.configurationUpdateHandler = nil
+        }
+    }
+
+    private func attachIfNeeded(to host: UIView) {
+        if superview !== host {
+            removeFromSuperview()
+            host.addSubview(self)
+            host.bringSubviewToFront(self)
+            translatesAutoresizingMaskIntoConstraints = false
+
+            let leading = leadingAnchor.constraint(equalTo: host.leadingAnchor, constant: 16)
+            let trailing = trailingAnchor.constraint(equalTo: host.trailingAnchor, constant: -16)
+            let bottom: NSLayoutConstraint
+            if #available(iOS 15.0, *) {
+                bottom = bottomAnchor.constraint(equalTo: host.keyboardLayoutGuide.topAnchor, constant: -10)
+            } else {
+                bottom = bottomAnchor.constraint(equalTo: host.safeAreaLayoutGuide.bottomAnchor, constant: -16)
+            }
+
+            NSLayoutConstraint.activate([leading, trailing, bottom])
+        }
+    }
+
+    private func animateIn() {
+        alpha = 0
+        transform = CGAffineTransform(translationX: 0, y: 10)
+        UIView.animate(withDuration: 0.24,
+                       delay: 0,
+                       usingSpringWithDamping: 0.86,
+                       initialSpringVelocity: 0.4,
+                       options: [.curveEaseOut, .allowUserInteraction],
+                       animations: {
+            self.alpha = 1
+            self.transform = .identity
+        }, completion: nil)
+    }
+
+    private func makeButton(for action: NativeEditorAction, tintColor: UIColor) -> UIButton {
+        var config = UIButton.Configuration.plain()
+        config.baseForegroundColor = tintColor
+        config.title = nil
+        config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)
+        config.preferredSymbolConfigurationForImage =
+          UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
+        config.background = .clear()
+
+        let button = UIButton(configuration: config, primaryAction: nil)
+        button.tintColor = tintColor
+
+        let symbolName = action.systemIcon ?? "circle"
+
+        // 🔑 Try custom SF Symbol as systemName first, then fall back to asset by name.
+        let image =
+          UIImage(systemName: symbolName) ??
+          UIImage(named: symbolName) ??
+          UIImage(systemName: "circle")
+
+        button.setImage(image, for: .normal)
+
+        button.accessibilityIdentifier = action.id
+        button.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
+        button.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
+
+        button.configurationUpdateHandler = { btn in
+            var cfg = btn.configuration
+            if btn.isHighlighted {
+                cfg?.background.backgroundColor = tintColor.withAlphaComponent(0.18)
+            } else {
+                cfg?.background.backgroundColor = .clear
+            }
+            btn.configuration = cfg
+        }
+
+        button.addTarget(self, action: #selector(handleActionTap(_:)), for: .touchUpInside)
+        return button
+    }
+
+    @objc private func handleActionTap(_ sender: UIButton) {
+        guard let id = sender.accessibilityIdentifier else { return }
+        onActionTapped?(id)
+    }
+
+    @objc private func handleTrailingTap(_ sender: UIButton) {
+        guard let id = trailingActionId ?? sender.accessibilityIdentifier else { return }
+        onActionTapped?(id)
+    }
+}
+
+@objc(NativeEditorToolbarPlugin)
+public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
+    public let identifier = "NativeEditorToolbarPlugin"
+    public let jsName = "NativeEditorToolbarPlugin"
+    public let pluginMethods: [CAPPluginMethod] = [
+        CAPPluginMethod(name: "present", returnType: CAPPluginReturnPromise),
+        CAPPluginMethod(name: "dismiss", returnType: CAPPluginReturnPromise)
+    ]
+
+    private var toolbar: NativeEditorToolbarView?
+
+    @objc func present(_ call: CAPPluginCall) {
+        let rawActions = call.getArray("actions", JSObject.self) ?? []
+        let actions = rawActions.compactMap(NativeEditorAction.init(jsObject:))
+        let trailingAction = call.getObject("trailingAction").flatMap(NativeEditorAction.init(jsObject:))
+
+        // We still read tintColor/backgroundColor for future flexibility,
+        // but the toolbar currently ignores them and uses theme colors.
+        _ = call.getString("tintColor")
+        _ = call.getString("backgroundColor")
+
+        DispatchQueue.main.async {
+            guard let host = self.hostView() else {
+                call.reject("Host view not found")
+                return
+            }
+
+            // If there are no actions and no trailing action, dismiss and clear toolbar
+            guard !actions.isEmpty || trailingAction != nil else {
+                self.toolbar?.dismiss(animated: true)
+                self.toolbar = nil
+                call.resolve()
+                return
+            }
+
+            let bar = self.toolbar ?? NativeEditorToolbarView()
+            bar.onActionTapped = { [weak self] id in
+                guard let self = self else { return }
+                self.notifyListeners("action", data: ["id": id])
+            }
+
+            bar.present(on: host,
+                        actions: actions,
+                        trailingAction: trailingAction,
+                        tintColor: nil,
+                        backgroundColor: nil)
+
+            self.toolbar = bar
+            call.resolve()
+        }
+    }
+
+    @objc func dismiss(_ call: CAPPluginCall) {
+        DispatchQueue.main.async {
+            self.toolbar?.dismiss(animated: true)
+            self.toolbar = nil
+            call.resolve()
+        }
+    }
+
+    private func hostView() -> UIView? {
+        if let parent = bridge?.viewController?.parent?.view {
+            return parent
+        }
+        return bridge?.viewController?.view
+    }
+}
+
+// MARK: - Color helper
+
+private extension String {
+    func toUIColor(defaultColor: UIColor) -> UIColor {
+        var hexString = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
+        if hexString.hasPrefix("#") {
+            hexString.removeFirst()
+        }
+
+        var rgbValue: UInt64 = 0
+        guard Scanner(string: hexString).scanHexInt64(&rgbValue) else {
+            return defaultColor
+        }
+
+        switch hexString.count {
+        case 6: // RRGGBB
+            return UIColor(
+                red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
+                green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
+                blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
+                alpha: 1.0
+            )
+        case 8: // RRGGBBAA
+            return UIColor(
+                red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
+                green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
+                blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
+                alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
+            )
+        default:
+            return defaultColor
+        }
+    }
+}

+ 3 - 5
src/main/frontend/handler/editor.cljs

@@ -1561,7 +1561,7 @@
 
         (if (assets-handler/exceed-limit-size? file)
           (do
-            (notification/show! [:div "Asset size shouldn't be larger than 100M"]
+            (notification/show! "Asset size shouldn't be larger than 100M"
                                 :warning
                                 false)
             (throw (ex-info "Asset size shouldn't be larger than 100M" {:file-name file-name})))
@@ -2301,8 +2301,7 @@
 
                  (catch :default ^js/Error e
                    (notification/show!
-                    [:p.content
-                     (util/format "Template insert error: %s" (.-message e))]
+                    (util/format "Template insert error: %s" (.-message e))
                     :error)))))))))))
 
 (defn template-on-chosen-handler
@@ -2419,8 +2418,7 @@
                 ;; cursor in other positions of :ke|y: or ke|y::, move to line end for inserting value.
                 (if (property-file/property-key-exist?-when-file-based format content property-key)
                   (notification/show!
-                   [:p.content
-                    (util/format "Property key \"%s\" already exists!" property-key)]
+                   (util/format "Property key \"%s\" already exists!" property-key)
                    :error)
                   (cursor/move-cursor-to-line-end input)))
 

+ 0 - 30
src/main/frontend/mobile/index.css

@@ -26,36 +26,6 @@
   }
 }
 
-#mobile-editor-toolbar {
-  @apply fixed bottom-0 left-0 w-full z-[99999] flex justify-between bg-background px-2;
-
-  will-change: transform;
-  transform: translateY(calc(-1 * var(--ls-native-kb-height, 0)));
-  opacity: var(--ls-native-toolbar-opacity, 0);
-  transition: transform 250ms ease-out, opacity 50ms ease-out;
-
-  button {
-    @apply flex items-center py-2 px-2;
-  }
-
-  .submenu {
-    @apply fixed left-0 bottom-0 w-full flex-row justify-evenly items-center z-10 bg-base-2
-    hidden overflow-x-auto overflow-y-hidden h-5 border;
-
-    &.show-submenu {
-      @apply flex;
-    }
-  }
-
-  .toolbar-commands {
-    @apply flex justify-between items-center overflow-y-hidden overflow-x-auto;
-
-    &::-webkit-scrollbar {
-      @apply h-1;
-    }
-  }
-}
-
 html.is-native-ipad {
   .action-bar {
     @apply w-[70%] min-w-[550px];

+ 2 - 0
src/main/frontend/mobile/util.cljs

@@ -25,11 +25,13 @@
 (defonce ui-local (registerPlugin "UILocal"))
 (defonce native-top-bar nil)
 (defonce native-bottom-sheet nil)
+(defonce native-editor-toolbar nil)
 (defonce native-selection-action-bar nil)
 (defonce ios-utils nil)
 (when (native-ios?)
   (set! native-top-bar (registerPlugin "NativeTopBarPlugin"))
   (set! native-bottom-sheet (registerPlugin "NativeBottomSheetPlugin"))
+  (set! native-editor-toolbar (registerPlugin "NativeEditorToolbarPlugin"))
   (set! native-selection-action-bar (registerPlugin "NativeSelectionActionBarPlugin"))
   (set! ios-utils (registerPlugin "Utils")))
 

+ 3 - 3
src/main/frontend/persist_db/browser.cljs

@@ -204,7 +204,7 @@
     (js/window.location.reload)
     (do
       (log/error :sqlite-error error)
-      (notification/show! [:div (str "SQLiteDB error: " error)] :error))))
+      (notification/show! (str "SQLiteDB error: " error) :error))))
 
 (defrecord InBrowser []
   protocol/PersistentDB
@@ -238,10 +238,10 @@
               (<export-db! repo data))))
         (p/catch (fn [error]
                    (log/error :export-db-error repo error "SQLiteDB save error")
-                   (notification/show! [:div (str "SQLiteDB save error: " error)] :error) {}))))
+                   (notification/show! (str "SQLiteDB save error: " error) :error) {}))))
 
   (<import-db [_this repo data]
     (-> (state/<invoke-db-worker-direct-pass :thread-api/import-db repo data)
         (p/catch (fn [error]
                    (log/error :import-db-error repo error "SQLiteDB import error")
-                   (notification/show! [:div (str "SQLiteDB import error: " error)] :error) {})))))
+                   (notification/show! (str "SQLiteDB import error: " error) :error) {})))))

+ 0 - 11
src/main/mobile/components/app.css

@@ -19,10 +19,6 @@ html.has-mobile-keyboard {
     @apply overflow-hidden
   }
 
-  #mobile-editor-toolbar {
-    @apply opacity-100;
-  }
-
   .app-popup, #main-content-container {
     padding-bottom: calc(var(--ls-native-kb-height, 0px) + 160px);
   }
@@ -266,13 +262,6 @@ ul {
   @apply text-base;
 }
 
-#mobile-editor-toolbar {
-  button {
-    @apply !bg-transparent hover:!bg-transparent active:!bg-transparent;
-    color: currentColor;
-  }
-}
-
 .no-repos {
   @apply pb-1;
 

+ 157 - 70
src/main/mobile/components/editor_toolbar.cljs

@@ -1,17 +1,18 @@
 (ns mobile.components.editor-toolbar
   "Mobile editor toolbar"
-  (:require [frontend.commands :as commands]
+  (:require [frontend.colors :as colors]
+            [frontend.commands :as commands]
             [frontend.components.svg :as svg]
             [frontend.handler.editor :as editor-handler]
             [frontend.mobile.camera :as mobile-camera]
             [frontend.mobile.haptics :as haptics]
+            [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
-            [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [goog.dom :as gdom]
             [logseq.common.util.page-ref :as page-ref]
-            [logseq.shui.ui :as shui]
+            [logseq.shui.hooks :as hooks]
             [mobile.components.recorder :as recorder]
             [mobile.init :as mobile-init]
             [mobile.state :as mobile-state]
@@ -19,36 +20,12 @@
             [rum.core :as rum]))
 
 (defn- blur-if-compositing
-  "Call blur on the textarea if it is in composition mode, let the IME commit the composing text"
+  "Call blur on the textarea if it is in composition mode so IME can commit composing text."
   []
   (when-let [edit-input-id (and (state/editor-in-composition?)
                                 (state/get-edit-input-id))]
-    (let [textarea-el (gdom/getElement edit-input-id)]
-      (.blur textarea-el))))
-
-(rum/defc command
-  [command-handler {:keys [icon class button-opts]} & [event?]]
-  (shui/button
-   (merge
-    {:variant :ghost
-     :on-pointer-down (fn [e]
-                        (util/stop e)
-                        (haptics/haptics)
-                        (if event?
-                          (command-handler e)
-                          (command-handler)))}
-    button-opts)
-   (if (string? icon)
-     (ui/icon icon {:size ui/icon-size :class class})
-     icon)))
-
-(rum/defc indent-outdent
-  [indent? icon]
-  (command
-   (fn []
-     (blur-if-compositing)
-     (editor-handler/indent-outdent indent?))
-   {:icon icon}))
+    (some-> (gdom/getElement edit-input-id)
+            (.blur))))
 
 (defn- insert-text
   [text opts]
@@ -62,53 +39,163 @@
                   text)]
       (commands/simple-insert! parent-id text' opts))))
 
-(defn commands
+(defn- indent-outdent-action
+  [indent?]
+  {:id (if indent? "indent" "outdent")
+   :title (if indent? "Indent" "Outdent")
+   :system-icon (if indent? "arrow.right" "arrow.left")
+   :icon (if indent? "arrow-right-to-arc" "arrow-left-to-arc")
+   :handler (fn []
+              (blur-if-compositing)
+              (editor-handler/indent-outdent indent?))})
+
+(defn- todo-action
+  []
+  {:id "todo"
+   :title "Todo"
+   :system-icon "checkmark.square"
+   :icon "checkbox"
+   :event? true
+   :handler (fn []
+              (blur-if-compositing)
+              (editor-handler/cycle-todo!))})
+
+(defn- tag-action
+  []
+  {:id "tag"
+   :title "Tag"
+   :system-icon "number"
+   :icon "hash"
+   :event? true
+   :handler #(insert-text "#" {})})
+
+(defn- page-ref-action
   []
-  [(command #(insert-text "#" {}) {:icon "hash"} true)
-   (command #(insert-text page-ref/left-and-right-brackets
+  {:id "page-ref"
+   :title "Reference"
+   ;; TODO: create sf symbol for brackets
+   :system-icon "parentheses"
+   :icon "brackets"
+   :event? true
+   :handler #(insert-text page-ref/left-and-right-brackets
                           {:backward-pos 2
                            :check-fn (fn [_ _ _]
                                        (let [input (state/get-input)
                                              new-pos (cursor/get-caret-pos input)]
                                          (state/set-editor-action-data! {:pos new-pos})
-                                         (commands/handle-step [:editor/search-page])))})
+                                         (commands/handle-step [:editor/search-page])))})})
+
+(defn- slash-action
+  []
+  {:id "slash"
+   :title "Slash"
+   :system-icon "command"
+   :icon "command"
+   :event? true
+   :handler #(insert-text "/" {})})
+
+(defn- camera-action
+  []
+  {:id "camera"
+   :title "Photo"
+   :system-icon "camera"
+   :icon "camera"
+   :event? true
+   :handler #(when-let [parent-id (state/get-edit-input-id)]
+               (mobile-camera/embed-photo parent-id))})
+
+(defn- audio-action
+  []
+  {:id "audio"
+   :title "Audio"
+   :system-icon "waveform"
+   :icon (svg/audio-lines 20)
+   :handler #(recorder/record!)})
+
+(defn- keyboard-action
+  []
+  {:id "keyboard"
+   :title "Hide"
+   :system-icon "keyboard.chevron.compact.down"
+   :icon "keyboard-show"
+   :handler #(p/do!
+              (editor-handler/save-current-block!)
+              (state/clear-edit!)
+              (mobile-init/keyboard-hide))})
+
+(defn- toolbar-actions
+  [quick-add?]
+  (let [audio (audio-action)
+        keyboard (keyboard-action)
+        main-actions (cond-> [(todo-action)
+                              (indent-outdent-action false)
+                              (indent-outdent-action true)
+                              (tag-action)
+                              (camera-action)
+                              (page-ref-action)
+                              (slash-action)]
+                       (not quick-add?) (conj audio))]
+    {:main main-actions
+     :trailing (if quick-add? audio keyboard)}))
+
+(defn- action->native
+  [{:keys [id title system-icon]}]
+  {:id id
+   :title (or title id)
+   :systemIcon system-icon})
+
+(defn- action-handlers
+  [main trailing]
+  (into {} (map (juxt :id :handler) (concat main (when trailing [trailing])))))
+
+(rum/defc native-toolbar
+  [show? {:keys [main trailing]}]
+  (let [handlers-ref (hooks/use-ref nil)
+        native-actions (mapv action->native main)
+        trailing-native (some-> trailing action->native)
+        plugin ^js mobile-util/native-editor-toolbar
+        should-show? (and show? (mobile-util/native-ios?) (some? plugin))]
+    (set! (.-current handlers-ref) (action-handlers main trailing))
+
+    (hooks/use-effect!
+     (fn []
+       (when (and (mobile-util/native-ios?) plugin)
+         (let [listener (.addListener plugin "action"
+                                      (fn [^js e]
+                                        (when-let [id (.-id e)]
+                                          (when-let [handler (get (.-current handlers-ref) id)]
+                                            (haptics/haptics)
+                                            (handler)))))]
+           (fn []
+             (cond
+               (and listener (.-remove listener)) ((.-remove listener))
+               listener (.then listener (fn [^js handle] (.remove handle))))))))
+     [])
+
+    (hooks/use-effect!
+     (fn []
+       (when (mobile-util/native-ios?)
+         (if should-show?
+           (.present plugin (clj->js {:actions native-actions
+                                      :trailingAction trailing-native
+                                      :tintColor (colors/get-accent-color)}))
+           (.dismiss plugin)))
+       #(when (mobile-util/native-ios?)
+          (.dismiss plugin)))
+     [should-show? native-actions trailing-native])
 
-            {:icon "brackets"} true)
-   (command #(insert-text "/" {}) {:icon "command"} true)])
+    [:<>]))
 
 (rum/defc mobile-bar < rum/reactive
   []
-  (when (and (util/mobile?)
-             (not (state/sub :editor/code-block-context))
-             (or (state/sub :editor/editing?)
-                 (= "app-keep-keyboard-open-input" (some-> js/document.activeElement (.-id)))))
-    (let [commands' (commands)
-          quick-add? (mobile-state/quick-add-open?)]
-      [:div#mobile-editor-toolbar
-       {:on-click #(util/stop %)}
-       [:div.toolbar-commands
-        ;; (command (editor-handler/move-up-down true) {:icon "arrow-bar-to-up"})
-        ;; (command (editor-handler/move-up-down false) {:icon "arrow-bar-to-down"})
-        (command #(do
-                    (blur-if-compositing)
-                    (editor-handler/cycle-todo!))
-                 {:icon "checkbox"} true)
-        (indent-outdent false "arrow-left-to-arc")
-        (indent-outdent true "arrow-right-to-arc")
-        ;; (command history/undo! {:icon "rotate" :class "rotate-180"} true)
-        ;; (command history/redo! {:icon "rotate-clockwise" :class "rotate-180"} true)
-        ;; (timestamp-submenu parent-id)
-        (for [command' commands']
-          command')
-        (command #(let [parent-id (state/get-edit-input-id)]
-                    (mobile-camera/embed-photo parent-id)) {:icon "camera"} true)
-        (when-not quick-add?
-          (command (fn [] (recorder/record!)) {:icon (svg/audio-lines 20)}))]
-       [:div.toolbar-hide-keyboard
-        (if quick-add?
-          (command (fn [] (recorder/record!))
-                   {:icon (svg/audio-lines 20)})
-          (command #(p/do!
-                     (editor-handler/save-current-block!)
-                     (state/clear-edit!)
-                     (mobile-init/keyboard-hide)) {:icon "keyboard-show"}))]])))
+  (let [editing? (state/sub :editor/editing?)
+        code-block? (state/sub :editor/code-block-context)
+        quick-add? (mobile-state/quick-add-open?)
+        keep-open? (= "app-keep-keyboard-open-input"
+                      (some-> js/document.activeElement (.-id)))
+        show? (and (util/mobile?)
+                   (not code-block?)
+                   (or editing? keep-open?))
+        actions (toolbar-actions quick-add?)]
+    (when (mobile-util/native-ios?)
+      (native-toolbar show? actions))))