Novus Technologies Novus Technologies design kit v0.9.0
Menu

iOS foundations

The Novus design system mapped onto native iOS: SwiftUI under the Human Interface Guidelines, the platform's standard stack. Every value is taken from tokens.css (rem converts at 16px per rem to pt), and the build rejects any colour on this page that is not a token value.

Design mapping, not a run-verified guide: this page maps tokens to SwiftUI and names its targets (iOS 18+, Swift 6, SwiftUI). Xcode cannot run in the docs build environment, so verification is the token-parity gate plus review, per constitution Principle VIII.

Reference screens

The target, before any code: a novapay operations dashboard on the mapping below, light and dark. These are reference renderings drawn entirely from tokens.css (the kit's own classes at device width), not device screenshots; the starter view further down reproduces them natively.

iOS reference dashboard, light
Dashboard, light
iOS reference dashboard, dark
Dashboard, dark (dynamic colors)

The prototype is the mapping made tappable: four screens with the large title following the tab, a working segmented control (Today, Week, Month), transaction filter chips, and the persisted theme toggle in Settings; navigation on native radio inputs so it works with JavaScript off.

Colors: dynamic light and dark from one definition

Define each semantic colour once with both appearances; the system switches them with the appearance, exactly like the web tokens' dark block.

import SwiftUI

extension Color {
    static func novus(_ light: UInt32, _ dark: UInt32) -> Color {
        Color(UIColor { $0.userInterfaceStyle == .dark
            ? UIColor(rgb: dark) : UIColor(rgb: light) })
    }

    // token: light hex, dark hex
    static let novusBg            = novus(0xFFFFFF, 0x0B1620) // --bg
    static let novusSurface       = novus(0xFFFFFF, 0x122131) // --surface
    static let novusSurfaceRaised = novus(0xF4F7FA, 0x18293B) // --bg-subtle / --surface-raised
    static let novusText          = novus(0x0E2336, 0xE7EDF3) // --text
    static let novusTextSecondary = novus(0x51606F, 0xAEBCC9) // --text-secondary
    static let novusTextMuted     = novus(0x6B7886, 0x8597A6) // --text-muted
    static let novusBorder        = novus(0xDCE3EB, 0x2A3B4D) // --border
    static let novusAccent        = novus(0x0070C0, 0x0070C0) // --accent (holds in dark)
    static let novusAccentText    = novus(0x00457A, 0x7CC0EE) // --accent-text
    static let novusSuccess       = novus(0x00A04A, 0x2BB266) // --success (judgements only)
    static let novusWarning       = novus(0xE8A300, 0xE8A300) // --warning (judgements only)
    static let novusDanger        = novus(0xC0473F, 0xD56A61) // --danger (risk always red)
}

extension UIColor {
    convenience init(rgb: UInt32) {
        self.init(red:   CGFloat((rgb >> 16) & 0xFF) / 255,
                  green: CGFloat((rgb >> 8)  & 0xFF) / 255,
                  blue:  CGFloat(rgb         & 0xFF) / 255, alpha: 1)
    }
}

Typography: Carlito only

Bundle Carlito 400 and 700 in the app (TTF from the design system master or Google Fonts; this npm package ships woff2 for the web only), register both in UIAppFonts, then map the kit scale with Dynamic Type support.

extension Font {
    // rem times 16 = pt, relative styles keep Dynamic Type scaling
    static let novus2xl  = Font.custom("Carlito-Bold",    size: 30, relativeTo: .largeTitle) // --text-2xl
    static let novusXl   = Font.custom("Carlito-Bold",    size: 24, relativeTo: .title)      // --text-xl
    static let novusLg   = Font.custom("Carlito-Bold",    size: 20, relativeTo: .title3)     // --text-lg
    static let novusMd   = Font.custom("Carlito-Bold",    size: 18, relativeTo: .headline)   // --text-md
    static let novusBase = Font.custom("Carlito-Regular", size: 16, relativeTo: .body)       // --text-base
    static let novusSm   = Font.custom("Carlito-Regular", size: 14, relativeTo: .subheadline)// --text-sm
    static let novusXs   = Font.custom("Carlito-Regular", size: 12, relativeTo: .caption)    // --text-xs
}

Spacing and radius

enum NovusSpace { // --space-1 .. --space-8
    static let s1: CGFloat = 4;  static let s2: CGFloat = 8
    static let s3: CGFloat = 12; static let s4: CGFloat = 16
    static let s5: CGFloat = 24; static let s6: CGFloat = 32
    static let s7: CGFloat = 48; static let s8: CGFloat = 64
}
enum NovusRadius { // --radius-sm/md/lg/xl
    static let sm: CGFloat = 6; static let md: CGFloat = 8
    static let lg: CGFloat = 12; static let xl: CGFloat = 16
}

Dark mode: dual trigger, same as the web

@main struct NovusApp: App {
    @AppStorage("novus-theme") private var theme = "system"
    var body: some Scene {
        WindowGroup {
            RootView()
                .preferredColorScheme(
                    theme == "light" ? .light :
                    theme == "dark"  ? .dark  : nil) // nil = follow the system
        }
    }
}
  • nil follows the system appearance; the persisted override is the second trigger, matching the web kit's novus-theme.js behaviour (same storage key, even).

Component themes: reusable modifiers

Wrap the kit's component forms as view modifiers once, so screens stay declarative and on-brand:

struct NovusCard: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding(NovusSpace.s4)
            .background(Color.novusSurface)
            .clipShape(RoundedRectangle(cornerRadius: NovusRadius.lg))
            .overlay(RoundedRectangle(cornerRadius: NovusRadius.lg)
                .stroke(Color.novusBorder, lineWidth: 1)) // border over shadow
    }
}

struct NovusChip: View { // self-describing judgement chip
    let label: String
    var tone: Color = .novusTextSecondary // judgement colours only on judgements
    var body: some View {
        HStack(spacing: NovusSpace.s1) {
            Circle().fill(tone).frame(width: 6, height: 6)
            Text(label).font(.novusSm)
        }
        .padding(.horizontal, NovusSpace.s3).padding(.vertical, NovusSpace.s1)
        .background(Capsule().stroke(Color.novusBorder, lineWidth: 1))
    }
}

extension View { func novusCard() -> some View { modifier(NovusCard()) } }

Starter view: the reference dashboard

struct DashboardView: View {
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: NovusSpace.s4) {
                    LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())],
                              spacing: NovusSpace.s3) {
                        Kpi(value: "1,284", label: "transactions today")
                        Kpi(value: "98.6%", label: "auth rate")
                        Kpi(value: "296",   label: "terminals online")
                        Kpi(value: "3",     label: "risk holds", tone: .novusDanger)
                    }
                    VStack(spacing: 0) {
                        Row(id: "TX-88412", sub: "T-0117, 10:24", amt: "84.50",
                            chip: NovusChip(label: "Settled", tone: .novusSuccess))
                        Row(id: "TX-88409", sub: "T-0117, 10:19", amt: "45.90",
                            chip: NovusChip(label: "Risk hold", tone: .novusDanger))
                    }.novusCard()
                    Button("Review risk holds") {}
                        .buttonStyle(.borderedProminent)
                        .tint(.novusAccent) // the one accent on this view
                }
                .padding(NovusSpace.s4)
            }
            .background(Color.novusBg)
            .navigationTitle("Dashboard")
        }
    }
}

private struct Kpi: View {
    let value: String; let label: String; var tone: Color = .novusText
    var body: some View {
        VStack(alignment: .leading, spacing: NovusSpace.s1) {
            Text(value).font(.novusXl).foregroundStyle(tone)
            Text(label).font(.novusSm).foregroundStyle(Color.novusTextSecondary)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .novusCard()
    }
}

private struct Row<Chip: View>: View {
    let id: String; let sub: String; let amt: String; let chip: Chip
    var body: some View {
        HStack(spacing: NovusSpace.s3) {
            VStack(alignment: .leading) {
                Text(id).font(.novusBase).fontWeight(.bold)
                Text(sub).font(.novusSm).foregroundStyle(Color.novusTextMuted)
            }
            Spacer()
            Text(amt).monospacedDigit() // amounts stay neutral
            chip
        }
        .padding(.vertical, NovusSpace.s3)
    }
}
  • Amounts stay neutral text; the chip carries the judgement colour, exactly like the reference screens.
  • The navigation bar's product identity is the placed novapay lockup image asset, never typed text.

Component correspondence

Kit componentSwiftUI equivalentNotes
.btn --primaryButton + .borderedProminent tint novusAccentOne per screen (one accent per view)
.btn --secondaryButton + .borderedOutline look, no tonal fill
.btn --ghostButton + .plain
.cardVStack in RoundedRectangle stroke novusBorderBorder over shadow, near-flat
.badge / chipsCapsule labelSelf-describing text; judgement colours only on judgements
.tableList / LazyVStack rowsDense rows, novusBorder dividers
.alertinline banner / .alert modifierRisk messages always novusDanger
.modal.sheet / .confirmationDialogConfirm far-right per Actions & placement
.field / .inputTextField + .textFieldStyle(.roundedBorder)Or a custom style on novusBorder
Stepper (progress)HStack of numbered capsulesNot SwiftUI's numeric Stepper; current step is the only accent

Rules that carry over unchanged

  • White (or near-black) ground, no gradients, elevation as a border stroke over shadow.
  • One accent per view; success and warning only where a judgement exists; risk messages always red; amounts always neutral.
  • Product names lowercase and solid-set (novapay); logos are placed assets from logos/ (PDF or PNG in the asset catalog), never redrawn, SF Symbols never substitute for a Novus mark.
  • Touch targets 44pt minimum (the HIG floor and the kit floor agree); respect Dynamic Type.