Theme.swift
207 lignes · 7928 octets
import SwiftUI // Port of android ui/theme/{Color,Type,Theme}.kt. // Palette PicLead — « ailiance brain » charte from the server (Server/tailwind.config.js): // white canvas, near-black ink, cool grays, mint accent, dedicated error red. // Link blue stays reserved for the « pending sync » dot (user spec). enum C2VColor { static let ink = Color(hexRGB: 0x0F1115) static let charcoal = Color(hexRGB: 0x1F2937) static let slate = Color(hexRGB: 0x475467) static let steel = Color(hexRGB: 0x667085) static let stone = Color(hexRGB: 0x98A2B3) static let canvas = Color(hexRGB: 0xFFFFFF) static let surfaceTeinte = Color(hexRGB: 0xF4F5F7) static let surfaceSoft = Color(hexRGB: 0xFAFBFC) static let hairline = Color(hexRGB: 0xE4E6EA) static let brandGreen = Color(hexRGB: 0x00D4A4) static let brandGreenDeep = Color(hexRGB: 0x00B389) static let brandGreenSoft = Color(hexRGB: 0xE6FAF4) static let brandError = Color(hexRGB: 0xE5484D) static let brandErrorSoft = Color(hexRGB: 0xFDECEA) static let brandWarn = Color(hexRGB: 0xE89642) /// Blue reserved for the « not synchronized » dot (user spec). static let link = Color(hexRGB: 0x057DBC) static let onPrimary = Color(hexRGB: 0xFFFFFF) // Historical aliases used by the migrated screens (Kotlin: Encre/Fond/…). static let inkSoft = charcoal static let body = steel static let canvasSoft = surfaceTeinte static let encre = ink static let fond = canvas static let surface = canvas static let bordure = hairline static let texteFaible = steel } private extension Color { init(hexRGB: UInt32) { self.init( red: Double((hexRGB >> 16) & 0xFF) / 255.0, green: Double((hexRGB >> 8) & 0xFF) / 255.0, blue: Double(hexRGB & 0xFF) / 255.0 ) } } // Android bundled Playfair (display serif), Lora (body serif) and Manrope // (sans labels). No fonts ship in the iOS target, so the system serif/sans // designs carry the same editorial split: serif for narrative, sans for labels. enum C2VFont { static let displayLarge = Font.system(size: 40, weight: .regular, design: .serif) static let displayMedium = Font.system(size: 32, weight: .regular, design: .serif) static let headlineMedium = Font.system(size: 26, weight: .regular, design: .serif) static let titleLarge = Font.system(size: 22, weight: .regular, design: .serif) static let titleMedium = Font.system(size: 16, weight: .bold) static let bodyLarge = Font.system(size: 16, weight: .regular, design: .serif) static let bodyMedium = Font.system(size: 14, weight: .regular) static let bodySmall = Font.system(size: 12, weight: .regular) static let labelLarge = Font.system(size: 16, weight: .bold) static let labelMedium = Font.system(size: 14, weight: .bold) static let labelSmall = Font.system(size: 12, weight: .bold) } /// design.md `button-primary`: square black CTA, white label. struct C2VPrimaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(C2VFont.labelLarge) .foregroundColor(C2VColor.onPrimary) .frame(maxWidth: .infinity) .padding(.vertical, 12) .padding(.horizontal, 20) .background(C2VColor.ink.opacity(configuration.isPressed ? 0.8 : 1)) } } /// design.md `button-outline`: white square with 1 px border, ink label. struct C2VOutlineButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(C2VFont.labelLarge) .foregroundColor(C2VColor.ink) .frame(maxWidth: .infinity) .padding(.vertical, 12) .padding(.horizontal, 20) .background(configuration.isPressed ? C2VColor.canvasSoft : C2VColor.canvas) .overlay(Rectangle().stroke(C2VColor.hairline, lineWidth: 1)) } } /// Compose `TextButton` with square shape — bare ink label. struct C2VTextButtonStyle: ButtonStyle { var color: Color = C2VColor.ink func makeBody(configuration: Configuration) -> some View { configuration.label .font(C2VFont.labelMedium) .foregroundColor(color.opacity(configuration.isPressed ? 0.6 : 1)) .padding(.vertical, 8) .padding(.horizontal, 8) } } /// 1 px hairline divider (design.md `hairline-divider`). struct C2VDivider: View { var body: some View { Rectangle() .fill(C2VColor.hairline) .frame(height: 1) } } /// Compose `AlertDialog` with square shape: dimmed backdrop (tap = dismiss request), /// square card on `Fond` with title, content and trailing text buttons. Stays /// presented until the caller clears its trigger state, so inline errors survive /// a failed confirm (system alerts always dismiss on any button). struct C2VDialog<DialogContent: View>: View { let title: String let confirmLabel: String let dismissLabel: String let onConfirm: () -> Void let onDismiss: () -> Void @ViewBuilder var content: () -> DialogContent var body: some View { ZStack { Color.black.opacity(0.35) .ignoresSafeArea() .onTapGesture(perform: onDismiss) VStack(alignment: .leading, spacing: 12) { Text(title) .font(C2VFont.titleMedium) .foregroundColor(C2VColor.ink) content() HStack(spacing: 4) { Spacer() Button(dismissLabel, action: onDismiss) .buttonStyle(C2VTextButtonStyle()) Button(confirmLabel, action: onConfirm) .buttonStyle(C2VTextButtonStyle()) } } .padding(20) .background(C2VColor.fond) .overlay(Rectangle().stroke(C2VColor.hairline, lineWidth: 1)) .padding(.horizontal, 32) } } } /// Square outlined text field, hairline border, ink border when focused /// (Compose `OutlinedTextField` with the Card2vcf color set). struct C2VTextField: View { let label: String var placeholder: String = "" @Binding var text: String var minLines: Int = 1 /// Hint under the field (Compose `supportingText`), e.g. « À vérifier — lecture incertaine ». var supportingText: String? = nil var isError: Bool = false @FocusState private var focused: Bool var body: some View { VStack(alignment: .leading, spacing: 4) { if !label.isEmpty { Text(label) .font(C2VFont.labelSmall) .foregroundColor(C2VColor.body) } field .textFieldStyle(.plain) .font(C2VFont.bodyLarge) .foregroundColor(C2VColor.ink) .tint(C2VColor.ink) .focused($focused) .padding(.horizontal, 12) .padding(.vertical, 10) .background(C2VColor.canvas) .overlay( Rectangle().stroke( isError ? C2VColor.brandError : (focused ? C2VColor.ink : C2VColor.hairline), lineWidth: 1 ) ) if let supportingText { Text(supportingText) .font(C2VFont.bodySmall) .foregroundColor(isError ? C2VColor.brandError : C2VColor.brandWarn) } } } @ViewBuilder private var field: some View { if minLines > 1 { TextField(placeholder, text: $text, axis: .vertical) .lineLimit(minLines...) } else { TextField(placeholder, text: $text) } } }
GitRust