a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Theme/Theme.swift

152 lines · 5387 bytes

  1import SwiftUI
  2import UIKit
  3import CoreText
  4
  5/// The web UI's design tokens (`internal/web/static/style.css` in
  6/// krz/gitbay), so the two surfaces read as one product. Every pair is
  7/// the stylesheet's light/dark value for the same variable.
  8extension Color {
  9
 10    nonisolated private static func paired(_ light: UInt32, _ dark: UInt32) -> Color {
 11        Color(UIColor { traits in
 12            UIColor(hex: traits.userInterfaceStyle == .dark ? dark : light)
 13        })
 14    }
 15
 16    /// --accent: links and focus.
 17    nonisolated static let gbAccent = paired(0x0000F0, 0x6272FF)
 18    /// --ok: open things, passing builds, verified signatures.
 19    nonisolated static let gbOK = paired(0x0A7D3C, 0x3FCE7A)
 20    /// --bad: closed without merging, failures, bad signatures.
 21    nonisolated static let gbBad = paired(0xC62828, 0xFF6B6B)
 22    /// --done: merged.
 23    nonisolated static let gbDone = paired(0x6B21A8, 0xC084FC)
 24    /// --warn: text-sized warnings.
 25    nonisolated static let gbWarn = paired(0xC2410C, 0xFF6B3D)
 26    /// --mark: the brand orange, bars and underlines.
 27    nonisolated static let gbMark = paired(0xE4572E, 0xFF6B3D)
 28    /// --diff-add / --diff-del: row grounds in diffs.
 29    nonisolated static let gbDiffAdd = paired(0xE4F6EA, 0x0D2A18)
 30    nonisolated static let gbDiffDel = paired(0xFDEAEA, 0x2C1113)
 31    /// --code-bg: code blocks.
 32    nonisolated static let gbCodeBackground = paired(0xF5F5F5, 0x050505)
 33    /// --fill-subtle: hunk headers and quiet fills.
 34    nonisolated static let gbFillSubtle = paired(0xECECEC, 0x161616)
 35    /// --faint: hairlines, row separators, and an empty calendar cell.
 36    nonisolated static let gbFaint = paired(0xEAEAEA, 0x1C1C1C)
 37    /// --bg: the page ground the accent is mixed toward. Chrome uses the
 38    /// system background; this is for blends that must match the web.
 39    nonisolated static let gbBackground = paired(0xFFFFFF, 0x0A0A0A)
 40    /// --line: control borders and table edges.
 41    nonisolated static let gbLine = paired(0xD4D4D4, 0x2E2E2E)
 42}
 43
 44extension UIColor {
 45    nonisolated convenience init(hex: UInt32) {
 46        self.init(
 47            red: CGFloat((hex >> 16) & 0xFF) / 255,
 48            green: CGFloat((hex >> 8) & 0xFF) / 255,
 49            blue: CGFloat(hex & 0xFF) / 255,
 50            alpha: 1
 51        )
 52    }
 53}
 54
 55/// The web's chip shape: --r-pill is 2px, not a capsule.
 56nonisolated let gbChipShape = RoundedRectangle(cornerRadius: 2)
 57
 58/// Atkinson Hyperlegible, the faces the web serves, scaled with Dynamic
 59/// Type. Falls back to the system face wherever the fonts fail to
 60/// register.
 61extension Font {
 62
 63    nonisolated static func gbSans(_ style: TextStyle) -> Font {
 64        .custom("Atkinson Hyperlegible Next", size: gbSize(style), relativeTo: style)
 65    }
 66
 67    nonisolated static func gbMono(_ style: TextStyle) -> Font {
 68        .custom("Atkinson Hyperlegible Mono", size: gbSize(style), relativeTo: style)
 69    }
 70
 71    /// Base sizes tracking the system text styles.
 72    nonisolated private static func gbSize(_ style: TextStyle) -> CGFloat {
 73        switch style {
 74        case .largeTitle: 34
 75        case .title: 28
 76        case .title2: 22
 77        case .title3: 20
 78        case .headline: 17
 79        case .body: 17
 80        case .callout: 16
 81        case .subheadline: 15
 82        case .footnote: 13
 83        case .caption: 12
 84        case .caption2: 11
 85        @unknown default: 17
 86        }
 87    }
 88}
 89
 90/// Registers the bundled faces once at launch. Registration failure is
 91/// survivable: Font.custom falls back to the system face.
 92enum GitbayFonts {
 93    static func register() {
 94        let names = [
 95            "AtkinsonNext-Regular", "AtkinsonNext-Bold", "AtkinsonNext-Semibold",
 96            "AtkinsonNext-Italic", "AtkinsonMono-Regular", "AtkinsonMono-Semibold",
 97        ]
 98        let urls = names.compactMap {
 99            Bundle.main.url(forResource: $0, withExtension: "ttf")
100        }
101        guard !urls.isEmpty else { return }
102        CTFontManagerRegisterFontURLs(urls as CFArray, .process, true, nil)
103    }
104}
105
106/// The web's notice idiom: a 3px stripe in the state's color down the
107/// left edge, not a tinted fill. `.error` uses --bad, `.notice` --ok;
108/// anything the server refused is --warn.
109struct GBNotice: View {
110    let text: String
111    let color: Color
112
113    init(_ text: String, _ color: Color = .gbBad) {
114        self.text = text
115        self.color = color
116    }
117
118    var body: some View {
119        HStack(alignment: .top, spacing: 10) {
120            Rectangle()
121                .fill(color)
122                .frame(width: 3)
123            Text(text)
124                .font(.gbSans(.subheadline))
125                .frame(maxWidth: .infinity, alignment: .leading)
126        }
127        .fixedSize(horizontal: false, vertical: true)
128        .padding(.vertical, 2)
129    }
130}
131
132/// The web's `.chip` recipe: state color, a 7% tint ground, a 35% tint
133/// border, 2px radius.
134struct GBChip: View {
135    let text: String
136    let color: Color
137
138    init(_ text: String, _ color: Color) {
139        self.text = text
140        self.color = color
141    }
142
143    var body: some View {
144        Text(text)
145            .font(.gbSans(.caption2).weight(.medium))
146            .foregroundStyle(color)
147            .padding(.horizontal, 7)
148            .padding(.vertical, 1)
149            .background(color.opacity(0.07), in: gbChipShape)
150            .overlay(gbChipShape.stroke(color.opacity(0.35), lineWidth: 1))
151    }
152}