import SwiftUI import UIKit extension Color { /// Light and dark values, as `web/src/tokens.css` has them. init(light: UInt32, dark: UInt32) { self.init(uiColor: UIColor { $0.userInterfaceStyle == .dark ? UIColor(hex: dark) : UIColor(hex: light) }) } static let success = Color(themed: \.success, light: 0x1a7f37, dark: 0x3fb950) /// Green to fill behind `onSolid` text: light's green in both of iOS's own, /// as dark's is too light for white on it, and a theme's own green, under its background. static let successFill = Color(themed: \.success, otherwise: UIColor(hex: 0x1a7f37)) /// Text on a solid fill of a state's colour, as the pages' `--on-solid`. static let onSolid = Color(themed: \.bg, otherwise: .white) /// The main button's fill: a theme's `primary`, as the pages' `.btn.primary`, /// and green in iOS's own colours, as the app draws what lands or sends. static let action = Color(themed: \.primary, otherwise: UIColor(hex: 0x1a7f37)) /// Text on the main button: a theme's `onPrimary`, which its contrast check holds to `primary`. static let onAction = Color(themed: \.onPrimary, otherwise: .white) static let danger = Color(themed: \.danger, light: 0xcf222e, dark: 0xf85149) static let done = Color(themed: \.done, light: 0x7048b5, dark: 0xb39af0) static let attention = Color(themed: \.attention, light: 0x9a6700, dark: 0xd29922) static let neutral = Color(themed: \.neutral, light: 0x636363, dark: 0x9198a1) static let accent = Color(themed: \.accent, light: 0x2f5fb3, dark: 0x7aa7f0) static let accentSoft = Color(themed: { Palette.mix($0.accent, 0.15, $0.bg) }, light: 0xeaeff7, dark: 0x222935) static func hex(_ string: String?) -> Color? { guard var s = string, s.hasPrefix("#") else { return nil } s.removeFirst() guard s.count == 6, let v = UInt32(s, radix: 16) else { return nil } return Color(uiColor: UIColor(hex: v)) } } /// Spaces, on the scale of `--space-*` in `web/src/tokens.css`: `s2` is its `--space-2`. enum Space { static let s0_5: CGFloat = 2 static let s1: CGFloat = 4 static let s1_5: CGFloat = 6 static let s2: CGFloat = 8 static let s2_5: CGFloat = 10 static let s3: CGFloat = 12 static let s3_5: CGFloat = 14 static let s4: CGFloat = 16 static let s5: CGFloat = 20 static let s6: CGFloat = 24 static let s7: CGFloat = 28 static let s9: CGFloat = 36 } enum Radius { static let small: CGFloat = 6 static let medium: CGFloat = 8 static let large: CGFloat = 12 /// A card of rows, or a sheet's box. static let card: CGFloat = 20 } /// An Octicon's size. enum Icon { static let xs: CGFloat = 12 static let sm: CGFloat = 14 static let md: CGFloat = 16 static let lg: CGFloat = 18 static let xl: CGFloat = 20 static let xxl: CGFloat = 24 } enum Size { enum Avatar { static let tiny: CGFloat = 16 static let xsmall: CGFloat = 20 static let small: CGFloat = 24 static let regular: CGFloat = 30 static let medium: CGFloat = 32 static let large: CGFloat = 44 static let xlarge: CGFloat = 64 /// One that is a toolbar's button: as big as iOS 26's round ones, and in /// earlier toolbars, which draw none, as an icon's room. static var toolbar: CGFloat { if #available(iOS 26, *) { large } else { medium } } } /// The width of a Markdown table's cell with more letters than fit on a line of it. static let tableCell: CGFloat = 260 static let tableCellLetters = 36 /// The least height of a row that opens a screen. static let row: CGFloat = 56 static let fileHeader: CGFloat = 52 /// The sign-in screen's field and button. static let signIn: CGFloat = 52 /// Room under a page for the button floating over its end. static let floatingClearance: CGFloat = 60 /// Where a row's text starts, past its padding and a regular avatar or /// icon square, for the divider under it to line up with. static let rowInset: CGFloat = Space.s4 + Avatar.regular + Space.s3 static let smallRowInset: CGFloat = Space.s4 + Avatar.small + Space.s3 } extension UIColor { convenience init(hex: UInt32) { self.init(red: CGFloat(hex >> 16 & 255) / 255, green: CGFloat(hex >> 8 & 255) / 255, blue: CGFloat(hex & 255) / 255, alpha: 1) } } /// How a pull or an issue shows its state. struct StateStyle { /// An image in the asset catalog: the Octicons the pages draw. let icon: String let color: Color let label: String init(kind: String?, state: String?, draft: Bool = false) { let pull = kind == "pull" switch state { case "merged": (icon, color, label) = ("oct-git-merge", .done, "Merged") case "closed": (icon, color, label) = pull ? ("oct-git-pull-request-closed", .danger, "Closed") : ("oct-issue-closed", .done, "Closed") default: if draft { (icon, color, label) = ("oct-git-pull-request-draft", .neutral, "Draft") } else { (icon, color, label) = (pull ? "oct-git-pull-request" : "oct-issue-opened", .success, "Open") } } } } /// The kind of thing an at:// URI names. func kind(ofURI uri: String?) -> String? { if uri?.contains("/dev.knotless.pull/") == true { return "pull" } if uri?.contains("/dev.knotless.issue/") == true { return "issue" } return nil } enum When { nonisolated(unsafe) private static let withFraction: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() nonisolated(unsafe) private static let plain = ISO8601DateFormatter() static func date(_ iso: String?) -> Date? { guard let iso else { return nil } return withFraction.date(from: iso) ?? plain.date(from: iso) } static func short(_ iso: String?) -> String { guard let date = date(iso) else { return "" } let s = Int((DevAccount.now ?? Date()).timeIntervalSince(date)) switch s { case ..<60: return "now" case ..<3600: return "\(s / 60)m" case ..<86400: return "\(s / 3600)h" case ..<(86400 * 30): return "\(s / 86400)d" case ..<(86400 * 365): return "\(s / (86400 * 30))mo" default: return "\(s / (86400 * 365))y" } } }