From 74b06d7fce01592fcd5f960637813ba99d230833 Mon Sep 17 00:00:00 2001 From: Camille Riquet Date: Mon, 19 Oct 2026 14:05:02 +0200 Subject: [PATCH] Add five colour themes: Lagoon, Sakura, Matcha, Synthwave and Dune --- .../Presentation/CodeScheme+Builtins.swift | 9 +++ .../BloomCore/Presentation/CodeScheme.swift | 2 +- .../Presentation/ColourTheme+Builtins.swift | 63 +++++++++++++++++++ .../BloomCore/Presentation/ColourTheme.swift | 2 +- .../TerminalScheme+Builtins.swift | 9 +++ .../Presentation/TerminalScheme.swift | 2 +- Tests/BloomCoreTests/ColourThemeTests.swift | 8 +++ docs/THEMES.md | 9 ++- 8 files changed, 99 insertions(+), 5 deletions(-) diff --git a/Sources/BloomCore/Presentation/CodeScheme+Builtins.swift b/Sources/BloomCore/Presentation/CodeScheme+Builtins.swift index e45bb5815..7c67093c5 100644 --- a/Sources/BloomCore/Presentation/CodeScheme+Builtins.swift +++ b/Sources/BloomCore/Presentation/CodeScheme+Builtins.swift @@ -3,6 +3,15 @@ extension CodeScheme { public static let charcoal = builtin( id: "charcoal", title: "Charcoal", background: .init(light: 0xFFFFFF, dark: 0x292C33) ) + public static let lagoon = themed(.lagoon) + public static let sakura = themed(.sakura) + public static let matcha = themed(.matcha) + public static let synthwave = themed(.synthwave) + public static let dune = themed(.dune) + + private static func themed(_ theme: ColourTheme) -> Self { + builtin(id: theme.id, title: theme.title, background: theme.surfaces.surface) + } /// A constant is a number as far as these schemes are concerned, and saying so is cheaper than /// keeping two copies of one pair in step. diff --git a/Sources/BloomCore/Presentation/CodeScheme.swift b/Sources/BloomCore/Presentation/CodeScheme.swift index 1148a11da..7675973d6 100644 --- a/Sources/BloomCore/Presentation/CodeScheme.swift +++ b/Sources/BloomCore/Presentation/CodeScheme.swift @@ -16,7 +16,7 @@ public struct CodeScheme: Codable, Hashable, Sendable, Identifiable { tokens[kind] ?? foreground } - public static let all: [Self] = [.bloom, .charcoal] + public static let all: [Self] = [.bloom, .charcoal, .lagoon, .sakura, .matcha, .synthwave, .dune] public static func find(_ key: String?, fallback: Self = .bloom) -> Self { all.first { $0.id == key } ?? fallback } diff --git a/Sources/BloomCore/Presentation/ColourTheme+Builtins.swift b/Sources/BloomCore/Presentation/ColourTheme+Builtins.swift index 5661bd77d..99a4352ea 100644 --- a/Sources/BloomCore/Presentation/ColourTheme+Builtins.swift +++ b/Sources/BloomCore/Presentation/ColourTheme+Builtins.swift @@ -31,4 +31,67 @@ extension ColourTheme { ), codeScheme: "charcoal", terminalScheme: "charcoal" ) + + // The light reading grounds of the five below (surface, raised, sunken) sit on Bloom's own + // luminance steps and carry their colour in hue alone, because `textTertiary` and the syntax + // comment clear 4.5 to 1 on white by a few hundredths and a darker ground fails + // `ColourThemeTests`. Cyan and green hold so much luminance that a tint at Bloom's step is + // invisible, so Lagoon and Matcha take their sidebar, selection and rule a step darker than + // Bloom's. The dark members have room, which is why these themes are louder at night. + + public static let lagoon = tinted(id: "lagoon", title: "Lagoon", surfaces: ThemeSurfaces( + surface: .init(light: 0xFBFFFF, dark: 0x052422), + raised: .init(light: 0xFFFFFF, dark: 0x10312F), + sunken: .init(light: 0xE6FFFE, dark: 0x062725), + sidebar: .init(light: 0xD6F4F0, dark: 0x072B29), + border: .init(light: 0x99D8D0, dark: 0x174543), + selected: .init(light: 0xBDEAE4, dark: 0x134240) + )) + + public static let sakura = tinted(id: "sakura", title: "Sakura", surfaces: ThemeSurfaces( + surface: .init(light: 0xFFFEFE, dark: 0x2F0C17), + raised: .init(light: 0xFFFFFF, dark: 0x4C1D2C), + sunken: .init(light: 0xFFF9FB, dark: 0x360D1B), + sidebar: .init(light: 0xFDF2F5, dark: 0x38101D), + border: .init(light: 0xF2D8E0, dark: 0x60293B), + selected: .init(light: 0xF7DFE7, dark: 0x63283C) + )) + + public static let matcha = tinted(id: "matcha", title: "Matcha", surfaces: ThemeSurfaces( + surface: .init(light: 0xFDFFFA, dark: 0x152405), + raised: .init(light: 0xFFFFFF, dark: 0x213110), + sunken: .init(light: 0xF0FFE0, dark: 0x172605), + sidebar: .init(light: 0xE5F3CD, dark: 0x1A2B07), + border: .init(light: 0xBCD996, dark: 0x2F4517), + selected: .init(light: 0xD5EBB6, dark: 0x2C4213) + )) + + public static let synthwave = tinted(id: "synthwave", title: "Synthwave", surfaces: ThemeSurfaces( + surface: .init(light: 0xFFFEFF, dark: 0x210E38), + raised: .init(light: 0xFFFFFF, dark: 0x392156), + sunken: .init(light: 0xFBF9FF, dark: 0x261040), + sidebar: .init(light: 0xF8F3FD, dark: 0x281341), + border: .init(light: 0xE5DAF3, dark: 0x4A2D6A), + selected: .init(light: 0xECE1F8, dark: 0x4B2D6D) + )) + + public static let dune = tinted(id: "dune", title: "Dune", surfaces: ThemeSurfaces( + surface: .init(light: 0xFFFEFD, dark: 0x201708), + raised: .init(light: 0xFFFFFF, dark: 0x372914), + sunken: .init(light: 0xFEFAF2, dark: 0x251A09), + sidebar: .init(light: 0xFCF4E7, dark: 0x281C0B), + border: .init(light: 0xECDDC6, dark: 0x49391F), + selected: .init(light: 0xF3E4CD, dark: 0x4B391E) + )) + + /// A theme whose code and terminal schemes share its id, so the editor and the shell stand + /// on its own ground rather than on Bloom's navy. + private static func tinted(id: String, title: String, surfaces: ThemeSurfaces) -> ColourTheme { + var surfaces = surfaces + surfaces.glassTint = surfaces.sidebar + surfaces.glassTintOpacity = 0.8 + return ColourTheme( + id: id, title: title, glass: .thick, surfaces: surfaces, codeScheme: id, terminalScheme: id + ) + } } diff --git a/Sources/BloomCore/Presentation/ColourTheme.swift b/Sources/BloomCore/Presentation/ColourTheme.swift index c34e46f3e..308d5b939 100644 --- a/Sources/BloomCore/Presentation/ColourTheme.swift +++ b/Sources/BloomCore/Presentation/ColourTheme.swift @@ -15,7 +15,7 @@ public struct ColourTheme: Codable, Hashable, Sendable, Identifiable, CaseIterab public let surfaces: ThemeSurfaces public static let defaultsKey = "colourTheme" - public static let allCases: [ColourTheme] = [.bloom, .charcoalGlass] + public static let allCases: [ColourTheme] = [.bloom, .charcoalGlass, .lagoon, .sakura, .matcha, .synthwave, .dune] public init( id: String, title: String, glass: ThemeGlass, surfaces: ThemeSurfaces, diff --git a/Sources/BloomCore/Presentation/TerminalScheme+Builtins.swift b/Sources/BloomCore/Presentation/TerminalScheme+Builtins.swift index 81b47b294..eae935567 100644 --- a/Sources/BloomCore/Presentation/TerminalScheme+Builtins.swift +++ b/Sources/BloomCore/Presentation/TerminalScheme+Builtins.swift @@ -5,6 +5,15 @@ extension TerminalScheme { public static let charcoal = builtin( id: "charcoal", title: "Charcoal", background: ColourTheme.charcoalGlass.surfaces.sunken ) + public static let lagoon = themed(.lagoon) + public static let sakura = themed(.sakura) + public static let matcha = themed(.matcha) + public static let synthwave = themed(.synthwave) + public static let dune = themed(.dune) + + private static func themed(_ theme: ColourTheme) -> Self { + builtin(id: theme.id, title: theme.title, background: theme.surfaces.sunken) + } private static func builtin(id: String, title: String, background: PaletteInk.Pair) -> Self { Self(id: id, title: title, diff --git a/Sources/BloomCore/Presentation/TerminalScheme.swift b/Sources/BloomCore/Presentation/TerminalScheme.swift index ddf4cbfa0..40c907e89 100644 --- a/Sources/BloomCore/Presentation/TerminalScheme.swift +++ b/Sources/BloomCore/Presentation/TerminalScheme.swift @@ -6,7 +6,7 @@ public struct TerminalScheme: Codable, Hashable, Sendable, Identifiable { public var light: GhosttyTheme public var dark: GhosttyTheme - public static let all: [Self] = [.bloom, .charcoal] + public static let all: [Self] = [.bloom, .charcoal, .lagoon, .sakura, .matcha, .synthwave, .dune] public static func find(_ key: String?, fallback: Self = .bloom) -> Self { all.first { $0.id == key } ?? fallback } diff --git a/Tests/BloomCoreTests/ColourThemeTests.swift b/Tests/BloomCoreTests/ColourThemeTests.swift index 6a8a2d4d1..c59db48ce 100644 --- a/Tests/BloomCoreTests/ColourThemeTests.swift +++ b/Tests/BloomCoreTests/ColourThemeTests.swift @@ -17,6 +17,14 @@ struct ColourThemeTests { #expect(decoded == theme) } + /// `find` falls back to Bloom's schemes, so a mistyped id would put the editor on Bloom's + /// navy inside another theme without anything failing. + @Test(arguments: ColourTheme.allCases) + func schemesExist(theme: ColourTheme) { + #expect(CodeScheme.all.contains { $0.id == theme.codeScheme }) + #expect(TerminalScheme.all.contains { $0.id == theme.terminalScheme }) + } + @Test func charcoalKeepsApprovedGlass() { let theme = ColourTheme.charcoalGlass #expect(theme.glass == .thick) diff --git a/docs/THEMES.md b/docs/THEMES.md index 8dfc9ed91..fadb282ad 100644 --- a/docs/THEMES.md +++ b/docs/THEMES.md @@ -12,7 +12,7 @@ The definitions are plain Swift data with Foundation `Codable` support: | File under `Sources/BloomCore/Presentation` | Purpose | | --- | --- | -| `ColourTheme+Builtins.swift` | Bloom and Charcoal Glass presets, including scheme references and font defaults | +| `ColourTheme+Builtins.swift` | Bloom, Charcoal Glass, Lagoon, Sakura, Matcha, Synthwave and Dune presets, including scheme references and font defaults | | `CodeScheme+Builtins.swift` | Code backgrounds, text, gutter, selection, caret, diff tints and token colours | | `TerminalScheme+Builtins.swift` | Complete light/dark terminal palettes using the existing `GhosttyTheme` data | | `ThemeTypography.swift` | Optional font family, point size and line-height multiplier for code and terminal | @@ -20,6 +20,11 @@ The definitions are plain Swift data with Foundation `Codable` support: | `TypographyOverrides.swift` | Code, terminal and conversation typography changes shared by every theme | Charcoal Glass keeps the stable key `neutral`, so previous selections still work. +Lagoon, Sakura, Matcha, Synthwave and Dune each share their id with a code and a terminal scheme +of their own, standing on the theme's surface and sunken colours. Their light reading grounds +keep Bloom's relative luminance steps, the surface within one percent of white, because the +tertiary ink and the syntax comment only just clear 4.5 to 1 there; the colour lives in the sidebar, selection and rule in light, and in every +ground in dark. The reason is written above them in `ColourTheme+Builtins.swift`. Colour pairs contain light and dark RGB integers. Swift definitions use `0xRRGGBB`; JSON uses decimal integers. Glass is `off`, `thin`, `regular` or `thick`, mapped to a tint over native AppKit sidebar material. Conversation typography retains its existing font names and size/spacing enums. @@ -73,4 +78,4 @@ preset file, VS Code colour themes for code and Ghostty or `.itermcolors` files can be adapted later without renderers knowing about external formats. Charcoal Glass keeps its approved dark glass tint `#212938` at 40% opacity. Bloom uses its -blue sidebar colour at 80% opacity, leaving a subtler native glass effect. +blue sidebar colour at 80% opacity, leaving a subtler native glass effect. The five tinted presets do the same with their own sidebar colour.