~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
166 lines · 6.4 KB Raw History
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"
        }
    }
}