132 lines
4.4 KiB
Swift
132 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#elseif canImport(AppKit)
|
|
import AppKit
|
|
#endif
|
|
|
|
// Shared cockpit components — every color/space comes from @Environment(\.tokens).
|
|
|
|
/// Decode image bytes to a SwiftUI `Image` on whichever Apple UI framework is
|
|
/// present. NSImage/UIImage decode HEIC + PNG natively. Shared by the asset
|
|
/// library tiles and the bump-screenshot card.
|
|
func decodeImage(_ data: Data) -> Image? {
|
|
#if canImport(UIKit)
|
|
guard let ui = UIImage(data: data) else { return nil }
|
|
return Image(uiImage: ui)
|
|
#elseif canImport(AppKit)
|
|
guard let ns = NSImage(data: data) else { return nil }
|
|
return Image(nsImage: ns)
|
|
#else
|
|
return nil
|
|
#endif
|
|
}
|
|
|
|
struct SectionLabel: View {
|
|
@Environment(\.tokens) private var t
|
|
let text: String
|
|
var body: some View {
|
|
Text(text.uppercased())
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.tracking(0.8)
|
|
.foregroundStyle(t.ink3)
|
|
}
|
|
}
|
|
|
|
struct SurfaceChip: View {
|
|
@Environment(\.tokens) private var t
|
|
let surface: Surface
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
Text(surface.glyph).font(.system(size: 10, weight: .bold))
|
|
Text(surface.label).font(.system(size: 11, weight: .medium))
|
|
}
|
|
.padding(.horizontal, 7).padding(.vertical, 3)
|
|
.foregroundStyle(surface.isNSFWAnchor ? t.anchorFg : t.ink2)
|
|
.background(surface.isNSFWAnchor ? t.anchorBg : t.bgElev)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
}
|
|
}
|
|
|
|
struct TosBadge: View {
|
|
@Environment(\.tokens) private var t
|
|
let tos: TosStatus
|
|
var body: some View {
|
|
let label: String = { switch tos { case .ok: return "K3 ok"; case .flagged: return "flagged"; case .blocked: return "blocked" } }()
|
|
Text(label)
|
|
.font(.system(size: 10, weight: .semibold))
|
|
.foregroundStyle(t.tos(tos))
|
|
.padding(.horizontal, 6).padding(.vertical, 2)
|
|
.background(t.tos(tos).opacity(0.14))
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
struct ConfidenceBadge: View {
|
|
@Environment(\.tokens) private var t
|
|
let value: Double
|
|
var body: some View {
|
|
Text(String(format: "%.2f", value))
|
|
.font(.system(size: 11, weight: .semibold, design: .monospaced))
|
|
.foregroundStyle(value >= 0.85 ? t.ok : (value >= 0.7 ? t.warn : t.accent))
|
|
}
|
|
}
|
|
|
|
// ImageRenderer can't capture ScrollView content; this is a real ScrollView in
|
|
// the app but a plain stack under the render flag, so headless renders are full.
|
|
private struct RenderModeKey: EnvironmentKey { static let defaultValue = false }
|
|
extension EnvironmentValues {
|
|
public var renderMode: Bool {
|
|
get { self[RenderModeKey.self] }
|
|
set { self[RenderModeKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
struct Scroll<Content: View>: View {
|
|
@Environment(\.renderMode) private var renderMode
|
|
@ViewBuilder var content: () -> Content
|
|
var body: some View {
|
|
if renderMode {
|
|
content()
|
|
} else {
|
|
ScrollView { content() }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pure-SwiftUI themed segmented control (no AppKit Picker — themable + renders
|
|
// in ImageRenderer).
|
|
struct Segmented<T: Hashable>: View {
|
|
@Environment(\.tokens) private var t
|
|
let options: [(String, T)]
|
|
@Binding var selection: T
|
|
var body: some View {
|
|
HStack(spacing: 2) {
|
|
ForEach(options, id: \.1) { label, value in
|
|
Text(label)
|
|
.font(.system(size: 11, weight: selection == value ? .semibold : .regular))
|
|
.foregroundStyle(selection == value ? t.accentFg : t.ink3)
|
|
.padding(.horizontal, 10).padding(.vertical, 4)
|
|
.background(selection == value ? t.accent : Color.clear)
|
|
.clipShape(Capsule())
|
|
.contentShape(Capsule())
|
|
.onTapGesture { selection = value }
|
|
}
|
|
}
|
|
.padding(3).background(t.bgElev).clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
struct Card<Content: View>: View {
|
|
@Environment(\.tokens) private var t
|
|
var elevated = false
|
|
@ViewBuilder var content: () -> Content
|
|
var body: some View {
|
|
content()
|
|
.padding(14)
|
|
.background(elevated ? t.bgElev : t.bgCard)
|
|
.clipShape(RoundedRectangle(cornerRadius: t.radius))
|
|
.overlay(RoundedRectangle(cornerRadius: t.radius).strokeBorder(t.line, lineWidth: 1))
|
|
}
|
|
}
|