| 1 | import SwiftUI |
| 2 | |
| 3 | // The feed's colour language, lifted verbatim from web/static/style.css's theme |
| 4 | // variables so native bubbles match the web feed pixel-for-pixel. One hard-coded |
| 5 | // theme for now — "classic", the web default — with the same light/dark pairs |
| 6 | // the CSS light-dark() calls carry. Real theme support (following the instance's |
| 7 | // chosen theme, reported by the webview) plugs in here later: it's one more |
| 8 | // ChatTheme value, not a redesign. |
| 9 | public struct ChatTheme: Sendable { |
| 10 | // One CSS light-dark() pair, resolved against the SwiftUI color scheme. |
| 11 | public struct Pair: Sendable { |
| 12 | public let light: Color |
| 13 | public let dark: Color |
| 14 | public func resolved(_ scheme: ColorScheme) -> Color { |
| 15 | scheme == .dark ? dark : light |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | public let bubbleMe: Pair |
| 20 | public let bubbleMeInk: Pair |
| 21 | public let bubbleThem: Pair |
| 22 | public let bubbleThemInk: Pair |
| 23 | public let accent: Pair |
| 24 | public let panel: Pair // --panel: the feed's surface |
| 25 | public let sidebar: Pair // --sidebar: the thread list's surface |
| 26 | |
| 27 | // style.css [data-theme="classic"]. |
| 28 | public static let classic = ChatTheme( |
| 29 | bubbleMe: Pair(light: Color(hex: 0x5666F0), dark: Color(hex: 0x5666F0)), |
| 30 | bubbleMeInk: Pair(light: .white, dark: .white), |
| 31 | bubbleThem: Pair(light: Color(hex: 0xEEF0F6), dark: Color(hex: 0x2A2E38)), |
| 32 | bubbleThemInk: Pair(light: Color(hex: 0x1C2333), dark: Color(hex: 0xE7E9F0)), |
| 33 | accent: Pair(light: Color(hex: 0x5666F0), dark: Color(hex: 0x7D8BFF)), |
| 34 | panel: Pair(light: Color(hex: 0xFFFFFF), dark: Color(hex: 0x1D2026)), |
| 35 | sidebar: Pair(light: Color(hex: 0xF8F9FC), dark: Color(hex: 0x1A1D22)) |
| 36 | ) |
| 37 | |
| 38 | // The web's other themes, transcribed from their [data-theme] blocks. |
| 39 | public static let litRoom = ChatTheme( |
| 40 | bubbleMe: Pair(light: Color(hex: 0xFFB23E), dark: Color(hex: 0xFFB23E)), |
| 41 | bubbleMeInk: Pair(light: Color(hex: 0x3A2412), dark: Color(hex: 0x2A1808)), |
| 42 | bubbleThem: Pair(light: Color(hex: 0xF1ECE4), dark: Color(hex: 0x2F2329)), |
| 43 | bubbleThemInk: Pair(light: Color(hex: 0x2A211B), dark: Color(hex: 0xF4E9DC)), |
| 44 | accent: Pair(light: Color(hex: 0xF0A93C), dark: Color(hex: 0xFFB23E)), |
| 45 | panel: Pair(light: Color(hex: 0xFFF8F0), dark: Color(hex: 0x241A1E)), |
| 46 | sidebar: Pair(light: Color(hex: 0xFBF1E6), dark: Color(hex: 0x1F161A)) |
| 47 | ) |
| 48 | |
| 49 | public static let lagoon = ChatTheme( |
| 50 | bubbleMe: Pair(light: Color(hex: 0x0E827E), dark: Color(hex: 0x16B3AD)), |
| 51 | bubbleMeInk: Pair(light: .white, dark: Color(hex: 0x042220)), |
| 52 | bubbleThem: Pair(light: Color(hex: 0xFFFFFF), dark: Color(hex: 0x123634)), |
| 53 | bubbleThemInk: Pair(light: Color(hex: 0x123A3A), dark: Color(hex: 0xD8EFEC)), |
| 54 | accent: Pair(light: Color(hex: 0xB85840), dark: Color(hex: 0xFF8A6A)), |
| 55 | panel: Pair(light: Color(hex: 0xF3FBFA), dark: Color(hex: 0x0C2A29)), |
| 56 | sidebar: Pair(light: Color(hex: 0xE7F6F4), dark: Color(hex: 0x082523)) |
| 57 | ) |
| 58 | |
| 59 | public static let sunny = ChatTheme( |
| 60 | bubbleMe: Pair(light: Color(hex: 0x2C7AB0), dark: Color(hex: 0x3AA0E8)), |
| 61 | bubbleMeInk: Pair(light: .white, dark: Color(hex: 0x04243A)), |
| 62 | bubbleThem: Pair(light: Color(hex: 0xFFFFFF), dark: Color(hex: 0x2E2616)), |
| 63 | bubbleThemInk: Pair(light: Color(hex: 0x3A2F1C), dark: Color(hex: 0xF6ECD6)), |
| 64 | accent: Pair(light: Color(hex: 0xFFC62E), dark: Color(hex: 0xFFC62E)), |
| 65 | panel: Pair(light: Color(hex: 0xFFFAF0), dark: Color(hex: 0x241D12)), |
| 66 | sidebar: Pair(light: Color(hex: 0xFFF4DF), dark: Color(hex: 0x1F190F)) |
| 67 | ) |
| 68 | |
| 69 | public static let linen = ChatTheme( |
| 70 | bubbleMe: Pair(light: Color(hex: 0x647686), dark: Color(hex: 0x7D94A8)), |
| 71 | bubbleMeInk: Pair(light: .white, dark: Color(hex: 0x0C1217)), |
| 72 | bubbleThem: Pair(light: Color(hex: 0xFBF9F4), dark: Color(hex: 0x2A2823)), |
| 73 | bubbleThemInk: Pair(light: Color(hex: 0x33302A), dark: Color(hex: 0xECE7DD)), |
| 74 | accent: Pair(light: Color(hex: 0x647686), dark: Color(hex: 0x93A8BA)), |
| 75 | panel: Pair(light: Color(hex: 0xF5F1EA), dark: Color(hex: 0x211F1B)), |
| 76 | sidebar: Pair(light: Color(hex: 0xECE6DC), dark: Color(hex: 0x1C1B17)) |
| 77 | ) |
| 78 | |
| 79 | /// The picker's catalog and the settings lookup. |
| 80 | public static let presets: [(id: String, title: String)] = [ |
| 81 | ("classic", "Classic"), ("litroom", "Lit Room"), ("lagoon", "Lagoon"), |
| 82 | ("sunny", "Sunny"), ("linen", "Linen"), |
| 83 | ] |
| 84 | |
| 85 | public static func named(_ id: String) -> ChatTheme { |
| 86 | switch id { |
| 87 | case "litroom": return .litRoom |
| 88 | case "lagoon": return .lagoon |
| 89 | case "sunny": return .sunny |
| 90 | case "linen": return .linen |
| 91 | default: return .classic |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | extension Color { |
| 97 | /// Color from a 0xRRGGBB literal — how the CSS hex values are transcribed. |
| 98 | init(hex: UInt32) { |
| 99 | self.init( |
| 100 | red: Double((hex >> 16) & 0xFF) / 255, |
| 101 | green: Double((hex >> 8) & 0xFF) / 255, |
| 102 | blue: Double(hex & 0xFF) / 255 |
| 103 | ) |
| 104 | } |
| 105 | } |
| 106 | |