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}