From ea44f1fd13cdc634a174062cdd403d0702a5765f Mon Sep 17 00:00:00 2001 From: phranck Date: Tue, 3 Feb 2026 22:27:59 +0100 Subject: [PATCH 1/2] Refactor: Consolidate 6 palette structs into PalettePreset enum + SystemPalette --- .../TUIkit/Extensions/View+Environment.swift | 2 +- .../Styling/Palettes/AmberPalette.swift | 91 ------ .../TUIkit/Styling/Palettes/BluePalette.swift | 92 ------ .../Styling/Palettes/GreenPalette.swift | 93 ------ .../Styling/Palettes/PalettePreset.swift | 279 ++++++++++++++++++ .../TUIkit/Styling/Palettes/RedPalette.swift | 92 ------ .../Styling/Palettes/VioletPalette.swift | 91 ------ .../Styling/Palettes/WhitePalette.swift | 80 ----- Sources/TUIkit/Styling/Theme.swift | 19 +- Sources/TUIkit/Styling/ThemeManager.swift | 2 +- .../TUIkitTests/PredefinedPaletteTests.swift | 10 +- .../ThemeManagerCyclingTests.swift | 4 +- 12 files changed, 293 insertions(+), 562 deletions(-) delete mode 100644 Sources/TUIkit/Styling/Palettes/AmberPalette.swift delete mode 100644 Sources/TUIkit/Styling/Palettes/BluePalette.swift delete mode 100644 Sources/TUIkit/Styling/Palettes/GreenPalette.swift create mode 100644 Sources/TUIkit/Styling/Palettes/PalettePreset.swift delete mode 100644 Sources/TUIkit/Styling/Palettes/RedPalette.swift delete mode 100644 Sources/TUIkit/Styling/Palettes/VioletPalette.swift delete mode 100644 Sources/TUIkit/Styling/Palettes/WhitePalette.swift diff --git a/Sources/TUIkit/Extensions/View+Environment.swift b/Sources/TUIkit/Extensions/View+Environment.swift index 92414e3..5d2f331 100644 --- a/Sources/TUIkit/Extensions/View+Environment.swift +++ b/Sources/TUIkit/Extensions/View+Environment.swift @@ -55,7 +55,7 @@ extension View { /// /// ```swift /// ContentView() - /// .palette(GreenPalette()) + /// .palette(SystemPalette(.green)) /// ``` /// /// - Parameter palette: The palette to apply. diff --git a/Sources/TUIkit/Styling/Palettes/AmberPalette.swift b/Sources/TUIkit/Styling/Palettes/AmberPalette.swift deleted file mode 100644 index f691316..0000000 --- a/Sources/TUIkit/Styling/Palettes/AmberPalette.swift +++ /dev/null @@ -1,91 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// AmberPalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Classic amber terminal palette (P3 phosphor). -/// -/// Inspired by terminals like the IBM 3278 and Wyse 50. -/// All colors are generated algorithmically from a single base hue (40°) -/// using HSL transformations. -public struct AmberPalette: BlockPalette { - public let id = "amber" - public let name = "Amber" - - /// The base hue used to generate all palette colors. - private static let baseHue: Double = 40 - - // Background - public let background: Color - - // Amber text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtly tinted - self.background = Color.hsl(hue, 30, 3) - - // Foregrounds: bright, saturated text - self.foreground = Color.hsl(hue, 100, 50) - self.foregroundSecondary = Color.hsl(hue, 100, 40) - self.foregroundTertiary = Color.hsl(hue, 100, 28) - - // Accent: lighter/brighter variant - self.accent = Color.hsl(hue + 5, 100, 60) - - // Semantic: hue-shifted from base - self.success = Color.hsl(Self.wrapHue(hue + 40), 100, 60) - self.warning = Color.hsl(Self.wrapHue(hue + 20), 100, 70) - self.error = Color.hsl(Self.wrapHue(hue - 25), 100, 60) - self.info = Color.hsl(Self.wrapHue(hue + 10), 100, 70) - - // UI elements - self.border = Color.hsl(hue, 33, 26) - - // Additional backgrounds - self.statusBarBackground = Color.hsl(hue, 35, 10) - self.appHeaderBackground = Color.hsl(hue, 35, 7) - self.overlayBackground = Color.hsl(hue, 30, 3) - } - -} - -// MARK: - Private Helpers - -private extension AmberPalette { - /// Wraps a hue value to the 0–360 range. - static func wrapHue(_ hue: Double) -> Double { - var wrapped = hue.truncatingRemainder(dividingBy: 360) - if wrapped < 0 { wrapped += 360 } - return wrapped - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == AmberPalette { - /// Amber terminal palette. - public static var amber: AmberPalette { AmberPalette() } -} diff --git a/Sources/TUIkit/Styling/Palettes/BluePalette.swift b/Sources/TUIkit/Styling/Palettes/BluePalette.swift deleted file mode 100644 index 6625bfd..0000000 --- a/Sources/TUIkit/Styling/Palettes/BluePalette.swift +++ /dev/null @@ -1,92 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// BluePalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Blue VFD terminal palette. -/// -/// Inspired by vintage vacuum fluorescent displays (VFDs) found in -/// audio equipment, cash registers, and instrument panels. -/// All colors are generated algorithmically from a single base hue (200°) -/// using HSL transformations. -public struct BluePalette: BlockPalette { - public let id = "blue" - public let name = "Blue" - - /// The base hue used to generate all palette colors. - private static let baseHue: Double = 200 - - // Background - public let background: Color - - // Blue text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtly tinted - self.background = Color.hsl(hue, 30, 3) - - // Foregrounds: bright, saturated text - self.foreground = Color.hsl(hue, 100, 50) - self.foregroundSecondary = Color.hsl(hue, 100, 40) - self.foregroundTertiary = Color.hsl(hue, 100, 30) - - // Accent: lighter/brighter variant - self.accent = Color.hsl(hue, 100, 60) - - // Semantic: hue-shifted from base - self.success = Color.hsl(Self.wrapHue(hue + 10), 100, 60) - self.warning = Color.hsl(Self.wrapHue(hue + 20), 100, 70) - self.error = Color.hsl(Self.wrapHue(hue - 185), 100, 60) - self.info = Color.hsl(Self.wrapHue(hue + 5), 100, 75) - - // UI elements - self.border = Color.hsl(hue, 33, 26) - - // Additional backgrounds - self.statusBarBackground = Color.hsl(hue, 35, 10) - self.appHeaderBackground = Color.hsl(hue, 35, 7) - self.overlayBackground = Color.hsl(hue, 30, 3) - } - -} - -// MARK: - Private Helpers - -private extension BluePalette { - /// Wraps a hue value to the 0–360 range. - static func wrapHue(_ hue: Double) -> Double { - var wrapped = hue.truncatingRemainder(dividingBy: 360) - if wrapped < 0 { wrapped += 360 } - return wrapped - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == BluePalette { - /// Blue VFD terminal palette. - public static var blue: BluePalette { BluePalette() } -} diff --git a/Sources/TUIkit/Styling/Palettes/GreenPalette.swift b/Sources/TUIkit/Styling/Palettes/GreenPalette.swift deleted file mode 100644 index 3447af9..0000000 --- a/Sources/TUIkit/Styling/Palettes/GreenPalette.swift +++ /dev/null @@ -1,93 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// GreenPalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Classic green terminal palette (P1 phosphor). -/// -/// Inspired by early CRT monitors like the IBM 5151 and Apple II. -/// All colors are generated algorithmically from a single base hue (120°) -/// using HSL transformations. -public struct GreenPalette: BlockPalette { - public let id = "green" - public let name = "Green" - - /// The base hue used to generate all palette colors. - private static let baseHue: Double = 120 - - // Background - public let background: Color - - // Green text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtly tinted - self.background = Color.hsl(hue, 30, 3) - - // Foregrounds: bright, saturated text - self.foreground = Color.hsl(hue, 100, 60) - self.foregroundSecondary = Color.hsl(hue, 67, 46) - self.foregroundTertiary = Color.hsl(hue, 64, 34) - - // Accent: lighter/brighter variant - self.accent = Color.hsl(hue, 100, 70) - - // Semantic: hue-shifted from base - self.success = Color.hsl(hue, 100, 60) - self.warning = Color.hsl(Self.wrapHue(hue - 45), 100, 60) - self.error = Color.hsl(Self.wrapHue(hue - 105), 100, 60) - self.info = Color.hsl(Self.wrapHue(hue + 45), 100, 60) - - // UI elements - self.border = Color.hsl(hue, 33, 26) - - // Additional backgrounds - self.statusBarBackground = Color.hsl(hue, 35, 10) - self.appHeaderBackground = Color.hsl(hue, 35, 7) - self.overlayBackground = Color.hsl(hue, 30, 3) - } - -} - -// MARK: - Private Helpers - -private extension GreenPalette { - /// Wraps a hue value to the 0–360 range. - static func wrapHue(_ hue: Double) -> Double { - var wrapped = hue.truncatingRemainder(dividingBy: 360) - if wrapped < 0 { wrapped += 360 } - return wrapped - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == GreenPalette { - /// The default palette (green). - public static var `default`: GreenPalette { GreenPalette() } - /// Green terminal palette. - public static var green: GreenPalette { GreenPalette() } -} diff --git a/Sources/TUIkit/Styling/Palettes/PalettePreset.swift b/Sources/TUIkit/Styling/Palettes/PalettePreset.swift new file mode 100644 index 0000000..2cd2952 --- /dev/null +++ b/Sources/TUIkit/Styling/Palettes/PalettePreset.swift @@ -0,0 +1,279 @@ +// 🖥️ TUIKit — Terminal UI Kit for Swift +// PalettePreset.swift +// +// Created by LAYERED.work +// CC BY-NC-SA 4.0 + +// MARK: - Palette Preset + +/// Built-in palette presets inspired by classic terminal phosphors. +/// +/// Each preset defines a complete color scheme generated algorithmically +/// from a base hue and hand-tuned HSL parameters. +/// +/// | Preset | Hue | Inspiration | +/// |----------|------|--------------------------------------------| +/// | `green` | 120° | IBM 5151, Apple II (P1 phosphor) | +/// | `amber` | 40° | IBM 3278, Wyse 50 (P3 phosphor) | +/// | `red` | 0° | Military/specialized, night-vision | +/// | `violet` | 270° | Retro computing, sci-fi terminals | +/// | `blue` | 200° | Vacuum fluorescent displays (VFDs) | +/// | `white` | 225° | DEC VT100/VT220 (P4 phosphor) | +/// +/// # Usage +/// +/// ```swift +/// let palette = SystemPalette(.amber) +/// paletteManager.setCurrent(SystemPalette(.green)) +/// ``` +public enum PalettePreset: String, CaseIterable, Sendable { + case green + case amber + case red + case violet + case blue + case white +} + +// MARK: - Built-In Palette + +/// A palette generated from a ``PalettePreset``. +/// +/// All colors are derived algorithmically from the preset's base hue +/// and hand-tuned HSL parameters. This single type replaces the six +/// individual palette structs (`GreenPalette`, `AmberPalette`, etc.). +/// +/// Custom palettes should conform to ``Palette`` or ``BlockPalette`` directly +/// instead of using this type. +public struct SystemPalette: BlockPalette { + public let id: String + public let name: String + + // Background + public let background: Color + + // Foreground hierarchy + public let foreground: Color + public let foregroundSecondary: Color + public let foregroundTertiary: Color + + // Accent + public let accent: Color + + // Semantic colors + public let success: Color + public let warning: Color + public let error: Color + public let info: Color + + // UI elements + public let border: Color + + // Additional backgrounds + public let statusBarBackground: Color + public let appHeaderBackground: Color + public let overlayBackground: Color + + /// Creates a palette from a preset. + /// + /// - Parameter preset: The built-in preset to use. + public init(_ preset: PalettePreset) { + let tuning = Tuning.for(preset) + let hue = tuning.baseHue + + self.id = preset.rawValue + self.name = preset.rawValue.capitalized + + // Backgrounds + self.background = Color.hsl(hue, tuning.bgSaturation, 3) + self.statusBarBackground = Color.hsl(hue, tuning.barSaturation, 10) + self.appHeaderBackground = Color.hsl(hue, tuning.barSaturation, 7) + self.overlayBackground = Color.hsl(hue, tuning.bgSaturation, 3) + + // Foregrounds + self.foreground = Color.hsl(tuning.fgHue, tuning.fgSaturation, tuning.fgLightness) + self.foregroundSecondary = Color.hsl(tuning.fgHue, tuning.fgSecSaturation, tuning.fgSecLightness) + self.foregroundTertiary = Color.hsl(tuning.fgHue, tuning.fgTerSaturation, tuning.fgTerLightness) + + // Accent + self.accent = Color.hsl(tuning.accentHue, tuning.accentSaturation, tuning.accentLightness) + + // Semantic colors + self.success = Color.hsl(tuning.successHue, tuning.successSaturation, tuning.successLightness) + self.warning = Color.hsl(tuning.warningHue, tuning.warningSaturation, tuning.warningLightness) + self.error = Color.hsl(tuning.errorHue, tuning.errorSaturation, tuning.errorLightness) + self.info = Color.hsl(tuning.infoHue, tuning.infoSaturation, tuning.infoLightness) + + // UI elements + self.border = Color.hsl(hue, tuning.borderSaturation, tuning.borderLightness) + } +} + +// MARK: - Tuning Data + +private extension SystemPalette { + /// Hand-tuned HSL parameters for each preset. + struct Tuning { + // Base + let baseHue: Double + let bgSaturation: Double + let barSaturation: Double + + // Foreground + let fgHue: Double + let fgSaturation: Double + let fgLightness: Double + let fgSecSaturation: Double + let fgSecLightness: Double + let fgTerSaturation: Double + let fgTerLightness: Double + + // Accent + let accentHue: Double + let accentSaturation: Double + let accentLightness: Double + + // Semantic + let successHue: Double + let successSaturation: Double + let successLightness: Double + let warningHue: Double + let warningSaturation: Double + let warningLightness: Double + let errorHue: Double + let errorSaturation: Double + let errorLightness: Double + let infoHue: Double + let infoSaturation: Double + let infoLightness: Double + + // Border + let borderSaturation: Double + let borderLightness: Double + } +} + +// MARK: - Preset Tuning Values + +private extension SystemPalette.Tuning { + /// Returns the tuning parameters for a given preset. + static func `for`(_ preset: PalettePreset) -> SystemPalette.Tuning { + switch preset { + case .green: + SystemPalette.Tuning( + baseHue: 120, bgSaturation: 30, barSaturation: 35, + fgHue: 120, fgSaturation: 100, fgLightness: 60, + fgSecSaturation: 67, fgSecLightness: 46, + fgTerSaturation: 64, fgTerLightness: 34, + accentHue: 120, accentSaturation: 100, accentLightness: 70, + successHue: 120, successSaturation: 100, successLightness: 60, + warningHue: wrapHue(120 - 45), warningSaturation: 100, warningLightness: 60, + errorHue: wrapHue(120 - 105), errorSaturation: 100, errorLightness: 60, + infoHue: wrapHue(120 + 45), infoSaturation: 100, infoLightness: 60, + borderSaturation: 33, borderLightness: 26 + ) + + case .amber: + SystemPalette.Tuning( + baseHue: 40, bgSaturation: 30, barSaturation: 35, + fgHue: 40, fgSaturation: 100, fgLightness: 50, + fgSecSaturation: 100, fgSecLightness: 40, + fgTerSaturation: 100, fgTerLightness: 28, + accentHue: 45, accentSaturation: 100, accentLightness: 60, + successHue: wrapHue(40 + 40), successSaturation: 100, successLightness: 60, + warningHue: wrapHue(40 + 20), warningSaturation: 100, warningLightness: 70, + errorHue: wrapHue(40 - 25), errorSaturation: 100, errorLightness: 60, + infoHue: wrapHue(40 + 10), infoSaturation: 100, infoLightness: 70, + borderSaturation: 33, borderLightness: 26 + ) + + case .red: + SystemPalette.Tuning( + baseHue: 0, bgSaturation: 30, barSaturation: 35, + fgHue: 0, fgSaturation: 100, fgLightness: 63, + fgSecSaturation: 60, fgSecLightness: 50, + fgTerSaturation: 62, fgTerLightness: 35, + accentHue: 0, accentSaturation: 100, accentLightness: 70, + successHue: wrapHue(0 + 30), successSaturation: 100, successLightness: 75, + warningHue: wrapHue(0 + 30), warningSaturation: 100, warningLightness: 70, + errorHue: 0, errorSaturation: 0, errorLightness: 100, + infoHue: 0, infoSaturation: 100, infoLightness: 80, + borderSaturation: 33, borderLightness: 26 + ) + + case .violet: + SystemPalette.Tuning( + baseHue: 270, bgSaturation: 30, barSaturation: 35, + fgHue: 270, fgSaturation: 80, fgLightness: 70, + fgSecSaturation: 70, fgSecLightness: 55, + fgTerSaturation: 60, fgTerLightness: 40, + accentHue: 270, accentSaturation: 85, accentLightness: 78, + successHue: wrapHue(270 + 120), successSaturation: 70, successLightness: 65, + warningHue: wrapHue(270 + 60), warningSaturation: 80, warningLightness: 70, + errorHue: wrapHue(270 + 180), errorSaturation: 85, errorLightness: 65, + infoHue: wrapHue(270 - 60), infoSaturation: 70, infoLightness: 70, + borderSaturation: 40, borderLightness: 25 + ) + + case .blue: + SystemPalette.Tuning( + baseHue: 200, bgSaturation: 30, barSaturation: 35, + fgHue: 200, fgSaturation: 100, fgLightness: 50, + fgSecSaturation: 100, fgSecLightness: 40, + fgTerSaturation: 100, fgTerLightness: 30, + accentHue: 200, accentSaturation: 100, accentLightness: 60, + successHue: wrapHue(200 + 10), successSaturation: 100, successLightness: 60, + warningHue: wrapHue(200 + 20), warningSaturation: 100, warningLightness: 70, + errorHue: wrapHue(200 - 185), errorSaturation: 100, errorLightness: 60, + infoHue: wrapHue(200 + 5), infoSaturation: 100, infoLightness: 75, + borderSaturation: 33, borderLightness: 26 + ) + + case .white: + SystemPalette.Tuning( + baseHue: 225, bgSaturation: 25, barSaturation: 20, + fgHue: 0, fgSaturation: 0, fgLightness: 91, + fgSecSaturation: 0, fgSecLightness: 69, + fgTerSaturation: 0, fgTerLightness: 47, + accentHue: 0, accentSaturation: 0, accentLightness: 100, + successHue: 120, successSaturation: 50, successLightness: 75, + warningHue: 40, warningSaturation: 60, warningLightness: 75, + errorHue: 0, errorSaturation: 60, errorLightness: 75, + infoHue: 210, infoSaturation: 60, infoLightness: 75, + borderSaturation: 0, borderLightness: 28 + ) + } + } + + /// Wraps a hue value to the 0–360 range. + private static func wrapHue(_ hue: Double) -> Double { + var wrapped = hue.truncatingRemainder(dividingBy: 360) + if wrapped < 0 { wrapped += 360 } + return wrapped + } +} + +// MARK: - Convenience Accessors + +extension BlockPalette where Self == SystemPalette { + /// The default palette (green). + public static var `default`: SystemPalette { SystemPalette(.green) } + + /// Green terminal palette (P1 phosphor). + public static var green: SystemPalette { SystemPalette(.green) } + + /// Amber terminal palette (P3 phosphor). + public static var amber: SystemPalette { SystemPalette(.amber) } + + /// Red terminal palette (night-vision). + public static var red: SystemPalette { SystemPalette(.red) } + + /// Violet terminal palette (retro/sci-fi). + public static var violet: SystemPalette { SystemPalette(.violet) } + + /// Blue VFD terminal palette. + public static var blue: SystemPalette { SystemPalette(.blue) } + + /// White terminal palette (P4 phosphor). + public static var white: SystemPalette { SystemPalette(.white) } +} diff --git a/Sources/TUIkit/Styling/Palettes/RedPalette.swift b/Sources/TUIkit/Styling/Palettes/RedPalette.swift deleted file mode 100644 index 95e8de9..0000000 --- a/Sources/TUIkit/Styling/Palettes/RedPalette.swift +++ /dev/null @@ -1,92 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// RedPalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Red terminal palette. -/// -/// Less common but used in some military and specialized applications. -/// Night-vision friendly with reduced eye strain in dark environments. -/// All colors are generated algorithmically from a single base hue (0°) -/// using HSL transformations. -public struct RedPalette: BlockPalette { - public let id = "red" - public let name = "Red" - - /// The base hue used to generate all palette colors. - private static let baseHue: Double = 0 - - // Background - public let background: Color - - // Red text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtly tinted - self.background = Color.hsl(hue, 30, 3) - - // Foregrounds: bright, saturated text - self.foreground = Color.hsl(hue, 100, 63) - self.foregroundSecondary = Color.hsl(hue, 60, 50) - self.foregroundTertiary = Color.hsl(hue, 62, 35) - - // Accent: lighter/brighter variant - self.accent = Color.hsl(hue, 100, 70) - - // Semantic: hue-shifted from base - self.success = Color.hsl(Self.wrapHue(hue + 30), 100, 75) - self.warning = Color.hsl(Self.wrapHue(hue + 30), 100, 70) - self.error = Color.hsl(0, 0, 100) - self.info = Color.hsl(hue, 100, 80) - - // UI elements - self.border = Color.hsl(hue, 33, 26) - - // Additional backgrounds - self.statusBarBackground = Color.hsl(hue, 35, 10) - self.appHeaderBackground = Color.hsl(hue, 35, 7) - self.overlayBackground = Color.hsl(hue, 30, 3) - } - -} - -// MARK: - Private Helpers - -private extension RedPalette { - /// Wraps a hue value to the 0–360 range. - static func wrapHue(_ hue: Double) -> Double { - var wrapped = hue.truncatingRemainder(dividingBy: 360) - if wrapped < 0 { wrapped += 360 } - return wrapped - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == RedPalette { - /// Red terminal palette. - public static var red: RedPalette { RedPalette() } -} diff --git a/Sources/TUIkit/Styling/Palettes/VioletPalette.swift b/Sources/TUIkit/Styling/Palettes/VioletPalette.swift deleted file mode 100644 index 75dbbe7..0000000 --- a/Sources/TUIkit/Styling/Palettes/VioletPalette.swift +++ /dev/null @@ -1,91 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// VioletPalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Violet terminal palette. -/// -/// Inspired by retro computing aesthetics and sci-fi terminal displays. -/// All colors are generated algorithmically from a single base hue (270°) -/// using HSL transformations. -public struct VioletPalette: BlockPalette { - public let id = "violet" - public let name = "Violet" - - /// The base hue used to generate all palette colors. - private static let baseHue: Double = 270 - - // Background - public let background: Color - - // Violet text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtly tinted - self.background = Color.hsl(hue, 30, 3) - - // Foregrounds: bright, saturated text - self.foreground = Color.hsl(hue, 80, 70) - self.foregroundSecondary = Color.hsl(hue, 70, 55) - self.foregroundTertiary = Color.hsl(hue, 60, 40) - - // Accent: lighter/brighter variant - self.accent = Color.hsl(hue, 85, 78) - - // Semantic: hue-shifted from base - self.success = Color.hsl(Self.wrapHue(hue + 120), 70, 65) - self.warning = Color.hsl(Self.wrapHue(hue + 60), 80, 70) - self.error = Color.hsl(Self.wrapHue(hue + 180), 85, 65) - self.info = Color.hsl(Self.wrapHue(hue - 60), 70, 70) - - // UI elements - self.border = Color.hsl(hue, 40, 25) - - // Additional backgrounds - self.statusBarBackground = Color.hsl(hue, 35, 10) - self.appHeaderBackground = Color.hsl(hue, 35, 7) - self.overlayBackground = Color.hsl(hue, 30, 3) - } - -} - -// MARK: - Private Helpers - -private extension VioletPalette { - /// Wraps a hue value to the 0–360 range. - static func wrapHue(_ hue: Double) -> Double { - var wrapped = hue.truncatingRemainder(dividingBy: 360) - if wrapped < 0 { wrapped += 360 } - return wrapped - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == VioletPalette { - /// Violet terminal palette. - public static var violet: VioletPalette { VioletPalette() } -} diff --git a/Sources/TUIkit/Styling/Palettes/WhitePalette.swift b/Sources/TUIkit/Styling/Palettes/WhitePalette.swift deleted file mode 100644 index a13bd64..0000000 --- a/Sources/TUIkit/Styling/Palettes/WhitePalette.swift +++ /dev/null @@ -1,80 +0,0 @@ -// 🖥️ TUIKit — Terminal UI Kit for Swift -// WhitePalette.swift -// -// Created by LAYERED.work -// CC BY-NC-SA 4.0 - -/// Classic white terminal palette (P4 phosphor). -/// -/// Inspired by terminals like the DEC VT100 and VT220. -/// Near-achromatic palette with a subtle cool blue tint (225°) in -/// backgrounds. Foregrounds are neutral gray. All colors are generated -/// algorithmically using HSL transformations. -public struct WhitePalette: BlockPalette { - public let id = "white" - public let name = "White" - - /// The base hue used for the subtle cool tint in backgrounds. - private static let baseHue: Double = 225 - - // Background - public let background: Color - - // White/gray text hierarchy - public let foreground: Color - public let foregroundSecondary: Color - public let foregroundTertiary: Color - - // Accent - public let accent: Color - - // Semantic colors - public let success: Color - public let warning: Color - public let error: Color - public let info: Color - - // UI elements - public let border: Color - - // Additional backgrounds - public let statusBarBackground: Color - public let appHeaderBackground: Color - public let overlayBackground: Color - - public init() { - let hue = Self.baseHue - - // Background: very dark, subtle cool tint - self.background = Color.hsl(hue, 25, 3) - - // Foregrounds: near-neutral gray (very low saturation) - self.foreground = Color.hsl(0, 0, 91) - self.foregroundSecondary = Color.hsl(0, 0, 69) - self.foregroundTertiary = Color.hsl(0, 0, 47) - - // Accent: pure white - self.accent = Color.hsl(0, 0, 100) - - // Semantic colors: subtle tints on neutral base - self.success = Color.hsl(120, 50, 75) - self.warning = Color.hsl(40, 60, 75) - self.error = Color.hsl(0, 60, 75) - self.info = Color.hsl(210, 60, 75) - - // UI elements: neutral gray - self.border = Color.hsl(0, 0, 28) - - // Additional backgrounds: subtle cool tint - self.statusBarBackground = Color.hsl(hue, 20, 10) - self.appHeaderBackground = Color.hsl(hue, 20, 7) - self.overlayBackground = Color.hsl(hue, 25, 3) - } -} - -// MARK: - Convenience Accessors - -extension BlockPalette where Self == WhitePalette { - /// White terminal palette. - public static var white: WhitePalette { WhitePalette() } -} diff --git a/Sources/TUIkit/Styling/Theme.swift b/Sources/TUIkit/Styling/Theme.swift index 0011b39..3ceffd8 100644 --- a/Sources/TUIkit/Styling/Theme.swift +++ b/Sources/TUIkit/Styling/Theme.swift @@ -23,7 +23,7 @@ import Foundation /// /// ```swift /// // Set the app palette -/// paletteManager.setCurrent(AmberPalette()) +/// paletteManager.setCurrent(SystemPalette(.amber)) /// /// // Use palette colors in views /// Text("Hello").foregroundColor(.palette.foreground) @@ -165,7 +165,7 @@ extension Palette { /// Environment key for the current palette. private struct PaletteKey: EnvironmentKey { - static let defaultValue: any Palette = GreenPalette() + static let defaultValue: any Palette = SystemPalette(.green) } extension EnvironmentValues { @@ -177,7 +177,7 @@ extension EnvironmentValues { /// WindowGroup { /// ContentView() /// } - /// .environment(\.palette, GreenPalette()) + /// .environment(\.palette, SystemPalette(.green)) /// ``` /// /// Access the palette in `renderToBuffer(context:)`: @@ -196,17 +196,10 @@ extension EnvironmentValues { /// Registry of available palettes. struct PaletteRegistry { - /// All available palettes in cycling order. + /// All available palettes in cycling order, built from ``PalettePreset``. /// /// Order: Green → Amber → Red → Violet → Blue → White - static let all: [any Palette] = [ - GreenPalette(), - AmberPalette(), - RedPalette(), - VioletPalette(), - BluePalette(), - WhitePalette(), - ] + static let all: [any Palette] = PalettePreset.allCases.map { SystemPalette($0) } /// Finds a palette by ID. static func palette(withId id: String) -> (any Palette)? { @@ -232,7 +225,7 @@ extension EnvironmentValues { /// ```swift /// let paletteManager = context.environment.paletteManager /// paletteManager.cycleNext() - /// paletteManager.setCurrent(AmberPalette()) + /// paletteManager.setCurrent(SystemPalette(.amber)) /// ``` public var paletteManager: ThemeManager { get { self[PaletteManagerKey.self] } diff --git a/Sources/TUIkit/Styling/ThemeManager.swift b/Sources/TUIkit/Styling/ThemeManager.swift index f825be2..3ebedf5 100644 --- a/Sources/TUIkit/Styling/ThemeManager.swift +++ b/Sources/TUIkit/Styling/ThemeManager.swift @@ -51,7 +51,7 @@ public protocol Cyclable: Sendable { /// // Access the palette manager via context.environment /// let paletteManager = context.environment.paletteManager /// paletteManager.cycleNext() -/// paletteManager.setCurrent(AmberPalette()) +/// paletteManager.setCurrent(SystemPalette(.amber)) /// let name = paletteManager.currentName /// ``` /// diff --git a/Tests/TUIkitTests/PredefinedPaletteTests.swift b/Tests/TUIkitTests/PredefinedPaletteTests.swift index e62a2b4..e25e8e0 100644 --- a/Tests/TUIkitTests/PredefinedPaletteTests.swift +++ b/Tests/TUIkitTests/PredefinedPaletteTests.swift @@ -31,7 +31,7 @@ struct GreenPaletteTests { @Test("Green palette block surfaces get progressively brighter") func greenBlockSurfaceLuminanceOrder() throws { - let palette = GreenPalette() + let palette = SystemPalette(.green) let bgLum = try #require(relativeLuminance(of: palette.background)) let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground)) let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground)) @@ -44,7 +44,7 @@ struct GreenPaletteTests { @Test("Green palette foregrounds get progressively dimmer") func greenForegroundLuminanceOrder() throws { - let palette = GreenPalette() + let palette = SystemPalette(.green) let fgLum = try #require(relativeLuminance(of: palette.foreground)) let fgSecLum = try #require(relativeLuminance(of: palette.foregroundSecondary)) let fgTerLum = try #require(relativeLuminance(of: palette.foregroundTertiary)) @@ -55,8 +55,6 @@ struct GreenPaletteTests { } - - // MARK: - Violet Palette Tests @Suite("Violet Palette Tests") @@ -64,7 +62,7 @@ struct VioletPaletteTests { @Test("Violet palette block surfaces get progressively brighter") func violetBlockSurfaceLuminanceOrder() throws { - let palette = VioletPalette() + let palette = SystemPalette(.violet) let bgLum = try #require(relativeLuminance(of: palette.background)) let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground)) let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground)) @@ -84,7 +82,7 @@ struct BluePaletteTests { @Test("Blue palette block surfaces get progressively brighter") func blueBlockSurfaceLuminanceOrder() throws { - let palette = BluePalette() + let palette = SystemPalette(.blue) let bgLum = try #require(relativeLuminance(of: palette.background)) let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground)) let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground)) diff --git a/Tests/TUIkitTests/ThemeManagerCyclingTests.swift b/Tests/TUIkitTests/ThemeManagerCyclingTests.swift index 2c3a096..b2639b4 100644 --- a/Tests/TUIkitTests/ThemeManagerCyclingTests.swift +++ b/Tests/TUIkitTests/ThemeManagerCyclingTests.swift @@ -99,8 +99,8 @@ struct ThemeManagerCyclingTests { @Test("ThemeManager with palette items returns currentPalette") func paletteManagerReturnsPalette() { let palettes: [any Cyclable] = [ - GreenPalette(), - AmberPalette(), + SystemPalette(.green), + SystemPalette(.amber), ] let manager = ThemeManager(items: palettes) #expect(manager.currentPalette != nil) From cb00b29d561a25f563b6ee0e98fa44c7186cd1d7 Mon Sep 17 00:00:00 2001 From: phranck Date: Tue, 3 Feb 2026 22:31:33 +0100 Subject: [PATCH 2/2] Refactor: Nest PalettePreset as SystemPalette.Preset --- .../Styling/Palettes/PalettePreset.swift | 67 +++++++++---------- Sources/TUIkit/Styling/Theme.swift | 4 +- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Sources/TUIkit/Styling/Palettes/PalettePreset.swift b/Sources/TUIkit/Styling/Palettes/PalettePreset.swift index 2cd2952..d82554e 100644 --- a/Sources/TUIkit/Styling/Palettes/PalettePreset.swift +++ b/Sources/TUIkit/Styling/Palettes/PalettePreset.swift @@ -4,40 +4,9 @@ // Created by LAYERED.work // CC BY-NC-SA 4.0 -// MARK: - Palette Preset +// MARK: - System Palette -/// Built-in palette presets inspired by classic terminal phosphors. -/// -/// Each preset defines a complete color scheme generated algorithmically -/// from a base hue and hand-tuned HSL parameters. -/// -/// | Preset | Hue | Inspiration | -/// |----------|------|--------------------------------------------| -/// | `green` | 120° | IBM 5151, Apple II (P1 phosphor) | -/// | `amber` | 40° | IBM 3278, Wyse 50 (P3 phosphor) | -/// | `red` | 0° | Military/specialized, night-vision | -/// | `violet` | 270° | Retro computing, sci-fi terminals | -/// | `blue` | 200° | Vacuum fluorescent displays (VFDs) | -/// | `white` | 225° | DEC VT100/VT220 (P4 phosphor) | -/// -/// # Usage -/// -/// ```swift -/// let palette = SystemPalette(.amber) -/// paletteManager.setCurrent(SystemPalette(.green)) -/// ``` -public enum PalettePreset: String, CaseIterable, Sendable { - case green - case amber - case red - case violet - case blue - case white -} - -// MARK: - Built-In Palette - -/// A palette generated from a ``PalettePreset``. +/// A palette generated from a built-in ``Preset``. /// /// All colors are derived algorithmically from the preset's base hue /// and hand-tuned HSL parameters. This single type replaces the six @@ -45,7 +14,35 @@ public enum PalettePreset: String, CaseIterable, Sendable { /// /// Custom palettes should conform to ``Palette`` or ``BlockPalette`` directly /// instead of using this type. +/// +/// # Usage +/// +/// ```swift +/// let palette = SystemPalette(.amber) +/// paletteManager.setCurrent(SystemPalette(.green)) +/// ``` public struct SystemPalette: BlockPalette { + // MARK: - Preset + + /// Built-in palette presets inspired by classic terminal phosphors. + /// + /// | Preset | Hue | Inspiration | + /// |----------|------|--------------------------------------------| + /// | `green` | 120° | IBM 5151, Apple II (P1 phosphor) | + /// | `amber` | 40° | IBM 3278, Wyse 50 (P3 phosphor) | + /// | `red` | 0° | Military/specialized, night-vision | + /// | `violet` | 270° | Retro computing, sci-fi terminals | + /// | `blue` | 200° | Vacuum fluorescent displays (VFDs) | + /// | `white` | 225° | DEC VT100/VT220 (P4 phosphor) | + public enum Preset: String, CaseIterable, Sendable { + case green + case amber + case red + case violet + case blue + case white + } + public let id: String public let name: String @@ -77,7 +74,7 @@ public struct SystemPalette: BlockPalette { /// Creates a palette from a preset. /// /// - Parameter preset: The built-in preset to use. - public init(_ preset: PalettePreset) { + public init(_ preset: Preset) { let tuning = Tuning.for(preset) let hue = tuning.baseHue @@ -157,7 +154,7 @@ private extension SystemPalette { private extension SystemPalette.Tuning { /// Returns the tuning parameters for a given preset. - static func `for`(_ preset: PalettePreset) -> SystemPalette.Tuning { + static func `for`(_ preset: SystemPalette.Preset) -> SystemPalette.Tuning { switch preset { case .green: SystemPalette.Tuning( diff --git a/Sources/TUIkit/Styling/Theme.swift b/Sources/TUIkit/Styling/Theme.swift index 3ceffd8..d6a0ffb 100644 --- a/Sources/TUIkit/Styling/Theme.swift +++ b/Sources/TUIkit/Styling/Theme.swift @@ -196,10 +196,10 @@ extension EnvironmentValues { /// Registry of available palettes. struct PaletteRegistry { - /// All available palettes in cycling order, built from ``PalettePreset``. + /// All available palettes in cycling order, built from ``SystemPalette/Preset``. /// /// Order: Green → Amber → Red → Violet → Blue → White - static let all: [any Palette] = PalettePreset.allCases.map { SystemPalette($0) } + static let all: [any Palette] = SystemPalette.Preset.allCases.map { SystemPalette($0) } /// Finds a palette by ID. static func palette(withId id: String) -> (any Palette)? {