johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
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"
}
}
}