Add iOS peek long-press context menu for image embeds
Introduces expo-bluesky-context-menu, a compositional Root/Trigger/Menu/MenuItem wrapper around UIContextMenuInteraction. The preview ViewController is sized to the image's true aspect ratio so tall/panorama previews don't stretch mid-lift, and menu icons are rasterized from the app's SVG icon set on the native side. The preview prop is a discriminated union (image today; video and externalCard reserved) so the same module can back those embeds in follow-ups. https://claude.ai/code/session_015REmux3R9uuEMMJUHxTyQT
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import UIKit
|
||||
|
||||
/// Builds a `UIMenu` from the JS-shipped item specs. Each item may carry an
|
||||
/// icon spec (SVG path data) which is rasterized via `IconRenderer`.
|
||||
enum MenuBuilder {
|
||||
/// Expected item shape from JS:
|
||||
/// {
|
||||
/// id: String,
|
||||
/// label: String,
|
||||
/// destructive?: Bool,
|
||||
/// disabled?: Bool,
|
||||
/// icon?: {
|
||||
/// paths: [String],
|
||||
/// viewBox: String,
|
||||
/// strokeWidth: Double
|
||||
/// }
|
||||
/// }
|
||||
static func build(items: [[String: Any]], onSelect: @escaping (String) -> Void) -> UIMenu {
|
||||
let actions: [UIMenuElement] = items.compactMap { spec in
|
||||
guard let id = spec["id"] as? String,
|
||||
let label = spec["label"] as? String else { return nil }
|
||||
|
||||
let destructive = (spec["destructive"] as? Bool) ?? false
|
||||
let disabled = (spec["disabled"] as? Bool) ?? false
|
||||
let image = icon(from: spec["icon"] as? [String: Any])
|
||||
|
||||
var attributes: UIMenuElement.Attributes = []
|
||||
if destructive { attributes.insert(.destructive) }
|
||||
if disabled { attributes.insert(.disabled) }
|
||||
|
||||
return UIAction(title: label, image: image, attributes: attributes) { _ in
|
||||
onSelect(id)
|
||||
}
|
||||
}
|
||||
return UIMenu(title: "", children: actions)
|
||||
}
|
||||
|
||||
private static func icon(from spec: [String: Any]?) -> UIImage? {
|
||||
guard let spec = spec,
|
||||
let paths = spec["paths"] as? [String], !paths.isEmpty else { return nil }
|
||||
let viewBox = (spec["viewBox"] as? String) ?? "0 0 24 24"
|
||||
let strokeWidth = CGFloat((spec["strokeWidth"] as? Double) ?? 0)
|
||||
let renderSpec = IconRenderer.Spec(
|
||||
paths: paths,
|
||||
viewBox: viewBox,
|
||||
strokeWidth: strokeWidth,
|
||||
pointSize: 24
|
||||
)
|
||||
return IconRenderer.image(for: renderSpec)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user