// AIMHintCard · N1 · v1 · shared hint card for native AIM mini apps (AIM apps rule 30). Hand-written once, vendored byte for byte. // Pairs with AIMMiniAppTokens.swift (generated), AIMVoxelModels.swift (generated) and AIMVoxelView.swift (hand-written). AppKit only. // Contract, lifted from the MEM PRISM pressure hint: title · one line of fact · up to three buttons · thin N1 border // · live character 40 pt on the left · appear = alpha 0 → 1 with a 6 pt rise over `motion-window-appear` (180 ms) // · the character assembles 700 ms after the first frame · auto-hide after `autoHide` seconds or after any action. // Reduce Motion: the card is shown at once and the character stays static; the gesture still works on click. // The card never activates the app and never moves the cursor: hosts put it into a non-activating panel // (`AIMHintCard.makePanel`) and order it front with `orderFrontRegardless()`. import AppKit public final class AIMHintCard: NSView { // MARK: contract public struct Action { public let label: String public let handler: () -> Void public init(_ label: String, _ handler: @escaping () -> Void) { self.label = label; self.handler = handler } } /// At most three buttons; extra actions are ignored and reported in the console once. public static let maxActions = 3 /// Live character on the left, one size for every product. public static let markSize: CGFloat = 40 public static let defaultWidth: CGFloat = 320 public static let inset: CGFloat = 16 public static let gap: CGFloat = 12 public static var reduceMotion: Bool { NSWorkspace.shared.accessibilityDisplayShouldReduceMotion } // MARK: palette and type · resolved from the generated tokens, hex mapped by the file-local helper in AIMVoxelView.swift private static func semantic(_ key: String, _ fallback: String) -> String { AIMMiniAppTokens.N1.semantic[key] ?? fallback } private static func component(_ key: String, _ fallback: String) -> String { AIMMiniAppTokens.N1.component[key] ?? fallback } public static let surface = NSColor(aimHex: component("hint-background", "#ffffff")) public static let ink = NSColor(aimHex: component("hint-text", "#202124")) public static let muted = NSColor(aimHex: semantic("text-secondary", "#6b6e75")) public static let border = NSColor(aimHex: component("hint-border", "#e8e9ed")) public static let hover = NSColor(aimHex: semantic("hover", "#f5f6f8")) public static let radius = CGFloat(AIMMiniAppTokens.number(component("hint-radius", "16px")) ?? 16) public static let controlRadius = CGFloat(AIMMiniAppTokens.number(semantic("radius-control", "8px")) ?? 8) /// Products register IBM Plex Mono under their own resource names; the card asks this hook first. /// Default: the Plex Mono faces if a host registered them, otherwise the system monospaced font. public static var font: (CGFloat, NSFont.Weight) -> NSFont = { size, weight in let name = weight >= .semibold ? "IBMPlexMono-SmBld" : weight >= .medium ? "IBMPlexMono-Medm" : "IBMPlexMono" return NSFont(name: name, size: size) ?? NSFont.monospacedSystemFont(ofSize: size, weight: weight) } /// Appear timing from the shared tokens: `motion-window-appear` (180 ms) and `motion-window-appear-shift` (6 pt). public static var appearDuration: TimeInterval { AIMMiniAppTokens.seconds(semantic("motion-window-appear", "180ms"), reducedMotion: reduceMotion) } public static var appearShift: CGFloat { reduceMotion ? 0 : CGFloat(AIMMiniAppTokens.number(semantic("motion-window-appear-shift", "6px")) ?? 6) } // MARK: state public let markView: AIMVoxelView public private(set) var title: String public private(set) var fact: String public private(set) var actions: [Action] /// Seconds until the card reports `expired`; 0 keeps it until an action or `dismiss(reason:)`. public let autoHide: TimeInterval /// Called once with the reason: `expired`, `escape`, `action: