d4fd15dc20
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
52 lines
1.7 KiB
Swift
52 lines
1.7 KiB
Swift
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)
|
|
}
|
|
}
|