Native Sidebar
This commit is contained in:
17
native-sidebar/ForumCapacitorNativeSidebar.podspec
Normal file
17
native-sidebar/ForumCapacitorNativeSidebar.podspec
Normal file
@@ -0,0 +1,17 @@
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ForumCapacitorNativeSidebar'
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
s.license = package['license']
|
||||
s.homepage = 'https://frm.so'
|
||||
s.author = package['author']
|
||||
s.source = { :git => 'https://frm.so', :tag => s.version.to_s }
|
||||
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
|
||||
s.ios.deployment_target = '14.0'
|
||||
s.dependency 'Capacitor'
|
||||
s.swift_version = '5.1'
|
||||
end
|
||||
24
native-sidebar/ios/Plugin/Info.plist
Normal file
24
native-sidebar/ios/Plugin/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
13
native-sidebar/ios/Plugin/NativeSidebarPlugin.m
Normal file
13
native-sidebar/ios/Plugin/NativeSidebarPlugin.m
Normal file
@@ -0,0 +1,13 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
// Define the plugin using the CAP_PLUGIN Macro, and
|
||||
// each method the plugin supports using the CAP_PLUGIN_METHOD macro.
|
||||
CAP_PLUGIN(NativeSidebarPlugin, "NativeSidebar",
|
||||
CAP_PLUGIN_METHOD(setData, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setTheme, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(open, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(close, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setEnabled, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(dismiss, CAPPluginReturnPromise);
|
||||
)
|
||||
88
native-sidebar/ios/Plugin/NativeSidebarPlugin.swift
Normal file
88
native-sidebar/ios/Plugin/NativeSidebarPlugin.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
/// Hosts a native left-edge drawer (the "sidebar") that floats above the
|
||||
/// Capacitor web view. The drawer's *content* is a second, transparent
|
||||
/// WKWebView that renders the existing `Sidebar.js` from data pushed in via
|
||||
/// `setData`. Taps inside that content web view are forwarded back to the main
|
||||
/// app as an `action` event, mirroring how the NativeInput plugin works.
|
||||
@objc(NativeSidebarPlugin)
|
||||
public class NativeSidebarPlugin: CAPPlugin {
|
||||
|
||||
private var drawer: SidebarDrawer?
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
private func hostView() -> UIView? {
|
||||
// Pin the drawer as a sibling of the web view inside the same parent so
|
||||
// it can float above the web content (same pattern as NativeInput).
|
||||
return webView?.superview ?? webView
|
||||
}
|
||||
|
||||
private func ensureDrawer() -> SidebarDrawer? {
|
||||
if let drawer = drawer { return drawer }
|
||||
guard let host = hostView() else { return nil }
|
||||
let drawer = SidebarDrawer(hostView: host, mainWebView: webView, parentVC: bridge?.viewController)
|
||||
drawer.onAction = { [weak self] data in
|
||||
self?.notifyListeners("action", data: data)
|
||||
}
|
||||
drawer.onStateChange = { [weak self] open in
|
||||
self?.notifyListeners("stateChange", data: ["open": open])
|
||||
}
|
||||
drawer.attach()
|
||||
self.drawer = drawer
|
||||
return drawer
|
||||
}
|
||||
|
||||
// MARK: Plugin methods
|
||||
|
||||
@objc func setData(_ call: CAPPluginCall) {
|
||||
// Forward the whole options object (minus Capacitor's callbackId) to the
|
||||
// content web view as the sidebar payload.
|
||||
var payload = (call.options as? [String: Any]) ?? [:]
|
||||
payload.removeValue(forKey: "callbackId")
|
||||
DispatchQueue.main.async {
|
||||
guard let drawer = self.ensureDrawer() else { call.reject("No host view"); return }
|
||||
drawer.setData(payload)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setTheme(_ call: CAPPluginCall) {
|
||||
let theme = call.getString("theme") ?? ""
|
||||
DispatchQueue.main.async {
|
||||
self.drawer?.setTheme(theme)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func open(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.ensureDrawer()?.open()
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func close(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.drawer?.close()
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setEnabled(_ call: CAPPluginCall) {
|
||||
let enabled = call.getBool("enabled") ?? true
|
||||
DispatchQueue.main.async {
|
||||
self.drawer?.setEnabled(enabled)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dismiss(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.drawer?.removeFromHost()
|
||||
self.drawer = nil
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
365
native-sidebar/ios/Plugin/SidebarDrawer.swift
Normal file
365
native-sidebar/ios/Plugin/SidebarDrawer.swift
Normal file
@@ -0,0 +1,365 @@
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
/// A native left-edge drawer that *pushes* the app content aside (rather than
|
||||
/// overlaying it). The sidebar panel sits at a fixed position on the left,
|
||||
/// behind the content; opening slides the whole content layer (the Capacitor
|
||||
/// web view plus any sibling overlays such as the native chat input bar) to the
|
||||
/// right to reveal it.
|
||||
///
|
||||
/// View layers in the host (`webView.superview`):
|
||||
/// • `panelHost` — the sidebar, inserted *below* the main web view. Fixed at
|
||||
/// the left, ~85% width. Hosts a transparent `contentWebView`
|
||||
/// that renders the existing `Sidebar.js`.
|
||||
/// • content — the main web view + other native overlays. Translated
|
||||
/// right by `currentOffset` when open (visual transform only,
|
||||
/// so the web layout never reflows).
|
||||
/// • `self` — a transparent gesture layer on top. Catches the left-edge
|
||||
/// open drag when closed, and tap / drag-to-close over the
|
||||
/// shifted content when open. Everything else falls through.
|
||||
class SidebarDrawer: UIView, WKScriptMessageHandler, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: Callbacks to the plugin
|
||||
var onAction: (([String: Any]) -> Void)?
|
||||
var onStateChange: ((Bool) -> Void)?
|
||||
|
||||
// MARK: Tunables
|
||||
private let panelRatio: CGFloat = 0.85
|
||||
private let edgeStripWidth: CGFloat = 20
|
||||
private let velocityToOpen: CGFloat = 500 // pt/s shortcut to decide snap
|
||||
|
||||
// MARK: Views
|
||||
/// The sidebar, kept behind the content and revealed as content slides away.
|
||||
private let panelHost = UIView()
|
||||
private var contentWebView: WKWebView!
|
||||
/// Owns the content web view so WebKit can find a presenting view controller
|
||||
/// for native form controls (e.g. the theme `<select>`'s picker).
|
||||
private let contentVC = UIViewController()
|
||||
|
||||
private weak var host: UIView?
|
||||
private weak var mainWebView: WKWebView?
|
||||
private weak var parentVC: UIViewController?
|
||||
|
||||
private var pan: UIPanGestureRecognizer!
|
||||
|
||||
// MARK: State
|
||||
private var isOpen = false
|
||||
private var isDragging = false
|
||||
private var isAnimating = false
|
||||
private var gestureEnabled = true
|
||||
/// How far (pt) the content is currently pushed right. 0 = closed.
|
||||
private var currentOffset: CGFloat = 0
|
||||
private var dragStartOffset: CGFloat = 0
|
||||
|
||||
/// Origin currently loaded into the content web view, so we only load once.
|
||||
private var loadedOrigin: String?
|
||||
/// True once the content page has signalled it is ready to receive data.
|
||||
private var contentReady = false
|
||||
/// Latest payload, buffered until the content page is ready.
|
||||
private var pendingData: [String: Any]?
|
||||
|
||||
private var panelWidth: CGFloat { (host?.bounds.width ?? bounds.width) * panelRatio }
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(hostView: UIView, mainWebView: WKWebView?, parentVC: UIViewController?) {
|
||||
self.host = hostView
|
||||
self.mainWebView = mainWebView
|
||||
self.parentVC = parentVC
|
||||
super.init(frame: .zero)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }
|
||||
|
||||
func attach() {
|
||||
guard let host = host else { return }
|
||||
|
||||
// Sidebar goes *below* the content so the content covers it when closed.
|
||||
panelHost.translatesAutoresizingMaskIntoConstraints = false
|
||||
if let webView = mainWebView, webView.superview === host {
|
||||
host.insertSubview(panelHost, belowSubview: webView)
|
||||
} else {
|
||||
host.insertSubview(panelHost, at: 0)
|
||||
}
|
||||
|
||||
// Transparent gesture layer on top of everything.
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
host.addSubview(self)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
panelHost.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
||||
panelHost.topAnchor.constraint(equalTo: host.topAnchor),
|
||||
panelHost.bottomAnchor.constraint(equalTo: host.bottomAnchor),
|
||||
panelHost.widthAnchor.constraint(equalTo: host.widthAnchor, multiplier: panelRatio),
|
||||
|
||||
contentVC.view.leadingAnchor.constraint(equalTo: panelHost.leadingAnchor),
|
||||
contentVC.view.trailingAnchor.constraint(equalTo: panelHost.trailingAnchor),
|
||||
contentVC.view.topAnchor.constraint(equalTo: panelHost.topAnchor),
|
||||
contentVC.view.bottomAnchor.constraint(equalTo: panelHost.bottomAnchor),
|
||||
|
||||
contentWebView.leadingAnchor.constraint(equalTo: contentVC.view.leadingAnchor),
|
||||
contentWebView.trailingAnchor.constraint(equalTo: contentVC.view.trailingAnchor),
|
||||
contentWebView.topAnchor.constraint(equalTo: contentVC.view.topAnchor),
|
||||
contentWebView.bottomAnchor.constraint(equalTo: contentVC.view.bottomAnchor),
|
||||
|
||||
leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
||||
trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
||||
topAnchor.constraint(equalTo: host.topAnchor),
|
||||
bottomAnchor.constraint(equalTo: host.bottomAnchor),
|
||||
])
|
||||
|
||||
// Register the content view controller so WebKit can find a presenter
|
||||
// for native UI (the theme <select> picker, etc.).
|
||||
if let parentVC = parentVC {
|
||||
parentVC.addChild(contentVC)
|
||||
contentVC.didMove(toParent: parentVC)
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromHost() {
|
||||
contentWebView?.configuration.userContentController.removeScriptMessageHandler(forName: "sidebar")
|
||||
contentVC.willMove(toParent: nil)
|
||||
contentVC.removeFromParent()
|
||||
applyOffset(0) // restore content to its resting position
|
||||
panelHost.removeFromSuperview()
|
||||
removeFromSuperview()
|
||||
}
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
private func setupViews() {
|
||||
backgroundColor = .clear
|
||||
panelHost.backgroundColor = .clear
|
||||
panelHost.clipsToBounds = true
|
||||
|
||||
// Transparent content web view hosting Sidebar.js (it paints its own
|
||||
// themed, opaque background).
|
||||
let config = WKWebViewConfiguration()
|
||||
config.websiteDataStore = .default()
|
||||
config.userContentController.add(WeakScriptMessageProxy(self), name: "sidebar")
|
||||
contentWebView = WKWebView(frame: .zero, configuration: config)
|
||||
contentWebView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentWebView.isOpaque = false
|
||||
contentWebView.backgroundColor = .clear
|
||||
contentWebView.scrollView.backgroundColor = .clear
|
||||
if #available(iOS 11.0, *) { contentWebView.scrollView.contentInsetAdjustmentBehavior = .never }
|
||||
|
||||
// The web view lives inside the content view controller's view so it has
|
||||
// an owning VC for presenting native controls.
|
||||
let container = contentVC.view!
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.backgroundColor = .clear
|
||||
panelHost.addSubview(container)
|
||||
container.addSubview(contentWebView)
|
||||
|
||||
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||
pan.delegate = self
|
||||
addGestureRecognizer(pan)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
tap.delegate = self
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// Keep the resting position correct as the host sizes (and on rotation),
|
||||
// but never fight an in-flight drag/animation.
|
||||
if !isDragging && !isAnimating {
|
||||
applyOffset(isOpen ? panelWidth : 0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Content translation
|
||||
|
||||
/// All content layers that should slide together — everything in the host
|
||||
/// except the sidebar and this gesture layer.
|
||||
private func contentSiblings() -> [UIView] {
|
||||
guard let host = host else { return [] }
|
||||
return host.subviews.filter { $0 !== self && $0 !== panelHost }
|
||||
}
|
||||
|
||||
private func applyOffset(_ x: CGFloat) {
|
||||
currentOffset = x
|
||||
let t = x == 0 ? .identity : CGAffineTransform(translationX: x, y: 0)
|
||||
for v in contentSiblings() { v.transform = t }
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
|
||||
func setData(_ payload: [String: Any]) {
|
||||
pendingData = payload
|
||||
if let origin = payload["uiOrigin"] as? String, origin != loadedOrigin {
|
||||
loadedOrigin = origin
|
||||
contentReady = false
|
||||
if let url = URL(string: "\(origin)/83947261/sidebar-host.html") {
|
||||
contentWebView.load(URLRequest(url: url))
|
||||
}
|
||||
}
|
||||
flushDataIfReady()
|
||||
}
|
||||
|
||||
func setTheme(_ theme: String) {
|
||||
guard contentReady, !theme.isEmpty else { return }
|
||||
let safe = theme.replacingOccurrences(of: "'", with: "")
|
||||
contentWebView.evaluateJavaScript("window.__sidebarSetTheme && window.__sidebarSetTheme('\(safe)')")
|
||||
}
|
||||
|
||||
func setEnabled(_ enabled: Bool) {
|
||||
gestureEnabled = enabled
|
||||
pan.isEnabled = enabled
|
||||
}
|
||||
|
||||
func open() { animate(open: true, velocity: 0) }
|
||||
func close() { animate(open: false, velocity: 0) }
|
||||
|
||||
// MARK: Data injection
|
||||
|
||||
private func flushDataIfReady() {
|
||||
guard contentReady, let payload = pendingData,
|
||||
let data = try? JSONSerialization.data(withJSONObject: payload),
|
||||
let json = String(data: data, encoding: .utf8) else { return }
|
||||
contentWebView.evaluateJavaScript("window.__sidebarSetData && window.__sidebarSetData(\(json))")
|
||||
}
|
||||
|
||||
// MARK: WKScriptMessageHandler
|
||||
|
||||
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard let body = message.body as? [String: Any],
|
||||
let action = body["action"] as? String else { return }
|
||||
|
||||
switch action {
|
||||
case "ready":
|
||||
contentReady = true
|
||||
flushDataIfReady()
|
||||
case "selectNetwork":
|
||||
onAction?(["type": "selectNetwork", "abbreviation": body["abbreviation"] as? String ?? ""])
|
||||
close()
|
||||
case "openProfile":
|
||||
onAction?(["type": "openProfile"])
|
||||
close()
|
||||
case "createNetwork":
|
||||
onAction?(["type": "createNetwork"])
|
||||
close()
|
||||
case "logout":
|
||||
onAction?(["type": "logout"])
|
||||
close()
|
||||
case "setTheme":
|
||||
// The content page applies the theme to itself locally; just relay
|
||||
// so the main app updates globally. Keep the drawer open.
|
||||
onAction?(["type": "setTheme", "value": body["value"] as? String ?? ""])
|
||||
case "close":
|
||||
close()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Gestures
|
||||
|
||||
@objc private func handleTap() {
|
||||
// A tap only reaches us (see hitTest) over the shifted content when open.
|
||||
if isOpen { close() }
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ g: UIPanGestureRecognizer) {
|
||||
guard let host = host else { return }
|
||||
switch g.state {
|
||||
case .began:
|
||||
isDragging = true
|
||||
superview?.bringSubviewToFront(self)
|
||||
dragStartOffset = currentOffset
|
||||
mainWebView?.allowsBackForwardNavigationGestures = false
|
||||
case .changed:
|
||||
let t = g.translation(in: host).x
|
||||
applyOffset(min(max(dragStartOffset + t, 0), panelWidth))
|
||||
case .ended, .cancelled, .failed:
|
||||
isDragging = false
|
||||
let v = g.velocity(in: host).x
|
||||
let target: Bool
|
||||
if abs(v) > velocityToOpen {
|
||||
target = v > 0
|
||||
} else if isOpen {
|
||||
// Was open → close once dragged left past ~1/3 (stay open otherwise).
|
||||
target = currentOffset > panelWidth * 2 / 3
|
||||
} else {
|
||||
// Was closed → open once dragged right past ~1/3.
|
||||
target = currentOffset > panelWidth / 3
|
||||
}
|
||||
animate(open: target, velocity: v)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func animate(open: Bool, velocity: CGFloat) {
|
||||
isAnimating = true
|
||||
if open {
|
||||
superview?.bringSubviewToFront(self)
|
||||
mainWebView?.allowsBackForwardNavigationGestures = false
|
||||
}
|
||||
let target: CGFloat = open ? panelWidth : 0
|
||||
let remaining = max(abs(target - currentOffset), 1)
|
||||
let springVelocity = min(abs(velocity) / remaining, 4)
|
||||
UIView.animate(withDuration: 0.32, delay: 0,
|
||||
usingSpringWithDamping: 0.9, initialSpringVelocity: springVelocity,
|
||||
options: [.allowUserInteraction, .beginFromCurrentState]) {
|
||||
self.applyOffset(target)
|
||||
} completion: { _ in
|
||||
self.isAnimating = false
|
||||
let changed = self.isOpen != open
|
||||
self.isOpen = open
|
||||
if !open { self.mainWebView?.allowsBackForwardNavigationGestures = true }
|
||||
if changed { self.onStateChange?(open) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UIGestureRecognizerDelegate
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ g: UIGestureRecognizer) -> Bool {
|
||||
guard gestureEnabled else { return g is UITapGestureRecognizer ? isOpen : false }
|
||||
guard let pan = g as? UIPanGestureRecognizer else { return true }
|
||||
// Start position is already gated by hitTest (closed → only the edge
|
||||
// strip reaches us; open → only the shifted content). Require a
|
||||
// horizontal-dominant drag so vertical scrolls pass through.
|
||||
let vel = pan.velocity(in: self)
|
||||
return abs(vel.x) >= abs(vel.y)
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ g: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: hitTest — gate which touches the gesture layer claims
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if isHidden || alpha == 0 { return nil }
|
||||
|
||||
if isOpen || isDragging {
|
||||
// The revealed sidebar (left of the content's left edge) stays
|
||||
// interactive — fall through to the sidebar web view beneath.
|
||||
if point.x < currentOffset { return nil }
|
||||
// Over the shifted content: claim the touch (tap / drag to close).
|
||||
return self
|
||||
}
|
||||
|
||||
// Closed: only the left strip is live (to start the open drag); every
|
||||
// other touch falls through to the content beneath.
|
||||
if gestureEnabled && point.x <= edgeStripWidth { return self }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Weak message-handler proxy
|
||||
|
||||
/// `WKUserContentController` retains its message handlers strongly. This weak
|
||||
/// proxy breaks the drawer ⇄ web view ⇄ controller retain cycle.
|
||||
private final class WeakScriptMessageProxy: NSObject, WKScriptMessageHandler {
|
||||
weak var target: WKScriptMessageHandler?
|
||||
init(_ target: WKScriptMessageHandler) { self.target = target }
|
||||
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
target?.userContentController(controller, didReceive: message)
|
||||
}
|
||||
}
|
||||
31
native-sidebar/package.json
Normal file
31
native-sidebar/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@forum/capacitor-native-sidebar",
|
||||
"version": "1.0.0",
|
||||
"description": "Native iOS left-edge drawer (sidebar) for the Forum app.",
|
||||
"main": "dist/plugin.cjs.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"files": [
|
||||
"dist/",
|
||||
"ios/Plugin/",
|
||||
"ForumCapacitorNativeSidebar.podspec"
|
||||
],
|
||||
"author": "Forum",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"capacitor",
|
||||
"plugin",
|
||||
"native",
|
||||
"ios",
|
||||
"sidebar",
|
||||
"drawer"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^7.0.0"
|
||||
},
|
||||
"capacitor": {
|
||||
"ios": {
|
||||
"src": "ios"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user