mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Merge pull request #48 from phranck/refactor/palette-essentials-block-palette
Refactor: Split Palette into Palette (13) + BlockPalette (3) protocol hierarchy
This commit is contained in:
@@ -76,7 +76,7 @@ extension BorderedView: Renderable {
|
||||
|
||||
/// Renders with half-block characters for block appearance.
|
||||
private func renderBlockStyle(buffer: FrameBuffer, innerWidth: Int, palette: any Palette) -> FrameBuffer {
|
||||
let containerBg = palette.containerBodyBackground
|
||||
let containerBg = palette.blockSurfaceBackground
|
||||
var lines: [String] = []
|
||||
|
||||
lines.append(BorderRenderer.blockTopBorder(innerWidth: innerWidth, color: containerBg))
|
||||
|
||||
@@ -123,24 +123,24 @@ public struct Color: Sendable, Equatable {
|
||||
/// Text("Hello").foregroundColor(.palette.accent)
|
||||
/// ```
|
||||
public enum Semantic {
|
||||
// Background colors
|
||||
// Background colors (Palette)
|
||||
public static let background = Color(value: .semantic(.background))
|
||||
public static let containerBodyBackground = Color(value: .semantic(.containerBodyBackground))
|
||||
public static let containerCapBackground = Color(value: .semantic(.containerCapBackground))
|
||||
public static let buttonBackground = Color(value: .semantic(.buttonBackground))
|
||||
public static let statusBarBackground = Color(value: .semantic(.statusBarBackground))
|
||||
public static let appHeaderBackground = Color(value: .semantic(.appHeaderBackground))
|
||||
public static let overlayBackground = Color(value: .semantic(.overlayBackground))
|
||||
|
||||
// Background colors (BlockPalette)
|
||||
public static let surfaceBackground = Color(value: .semantic(.surfaceBackground))
|
||||
public static let surfaceHeaderBackground = Color(value: .semantic(.surfaceHeaderBackground))
|
||||
public static let elevatedBackground = Color(value: .semantic(.elevatedBackground))
|
||||
|
||||
// Foreground colors
|
||||
public static let foreground = Color(value: .semantic(.foreground))
|
||||
public static let foregroundSecondary = Color(value: .semantic(.foregroundSecondary))
|
||||
public static let foregroundTertiary = Color(value: .semantic(.foregroundTertiary))
|
||||
public static let foregroundPlaceholder = Color(value: .semantic(.foregroundPlaceholder))
|
||||
|
||||
// Accent colors
|
||||
public static let accent = Color(value: .semantic(.accent))
|
||||
public static let accentSecondary = Color(value: .semantic(.accentSecondary))
|
||||
|
||||
// Status colors
|
||||
public static let success = Color(value: .semantic(.success))
|
||||
@@ -294,14 +294,69 @@ public struct Color: Sendable, Equatable {
|
||||
adjusted(by: -amount)
|
||||
}
|
||||
|
||||
// MARK: - RGB Conversion
|
||||
|
||||
/// The RGB components of this color.
|
||||
///
|
||||
/// Converts any color type to its RGB representation:
|
||||
/// - `.rgb` — returned directly
|
||||
/// - `.standard` / `.bright` — mapped to xterm standard RGB values
|
||||
/// - `.palette256` — mapped to xterm 256-color palette RGB values
|
||||
/// - `.semantic` — returns nil (must be resolved first via ``resolve(with:)``)
|
||||
public var rgbComponents: (red: UInt8, green: UInt8, blue: UInt8)? {
|
||||
switch value {
|
||||
case .rgb(let red, let green, let blue):
|
||||
return (red, green, blue)
|
||||
case .standard(let ansi):
|
||||
return ansi.rgbValues
|
||||
case .bright(let ansi):
|
||||
return ansi.brightRGBValues
|
||||
case .palette256(let index):
|
||||
return Self.palette256ToRGB(index)
|
||||
case .semantic:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a 256-color palette index to RGB values.
|
||||
///
|
||||
/// - Indices 0–7: standard ANSI colors
|
||||
/// - Indices 8–15: bright ANSI colors
|
||||
/// - Indices 16–231: 6×6×6 color cube
|
||||
/// - Indices 232–255: grayscale ramp
|
||||
private static func palette256ToRGB(_ index: UInt8) -> (red: UInt8, green: UInt8, blue: UInt8) {
|
||||
switch index {
|
||||
case 0...7:
|
||||
guard let ansi = ANSIColor(rawValue: index) else { return (0, 0, 0) }
|
||||
return ansi.rgbValues
|
||||
case 8...15:
|
||||
guard let ansi = ANSIColor(rawValue: index - 8) else { return (0, 0, 0) }
|
||||
return ansi.brightRGBValues
|
||||
case 16...231:
|
||||
// 6×6×6 color cube: index = 16 + 36*r + 6*g + b (each 0–5)
|
||||
let cubeIndex = index - 16
|
||||
let cubeRed = cubeIndex / 36
|
||||
let cubeGreen = (cubeIndex % 36) / 6
|
||||
let cubeBlue = cubeIndex % 6
|
||||
let channelMap: [UInt8] = [0, 95, 135, 175, 215, 255]
|
||||
return (channelMap[Int(cubeRed)], channelMap[Int(cubeGreen)], channelMap[Int(cubeBlue)])
|
||||
default:
|
||||
// Grayscale ramp: 232–255 → 8, 18, 28, ..., 238
|
||||
let gray = UInt8(8 + Int(index - 232) * 10)
|
||||
return (gray, gray, gray)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjusts brightness by a signed amount.
|
||||
///
|
||||
/// Positive values lighten, negative values darken.
|
||||
/// Positive values lighten, negative values darken. Works with all color types
|
||||
/// (ANSI, 256-palette, RGB) by converting to RGB first. The result is always
|
||||
/// an RGB color.
|
||||
///
|
||||
/// - Parameter amount: The adjustment amount (-1 to 1).
|
||||
/// - Returns: The adjusted color, or self if not an RGB color.
|
||||
/// - Returns: The adjusted color as RGB, or self if semantic (unresolved).
|
||||
private func adjusted(by amount: Double) -> Self {
|
||||
guard case .rgb(let red, let green, let blue) = value else {
|
||||
guard let (red, green, blue) = rgbComponents else {
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -316,12 +371,13 @@ public struct Color: Sendable, Equatable {
|
||||
/// Returns a color with adjusted opacity (simulated via color mixing).
|
||||
///
|
||||
/// Since terminals don't support true transparency, this mixes
|
||||
/// the color with black to simulate opacity.
|
||||
/// the color with black to simulate opacity. Works with all color types
|
||||
/// by converting to RGB first.
|
||||
///
|
||||
/// - Parameter opacity: The opacity (0-1).
|
||||
/// - Returns: A color simulating the given opacity.
|
||||
/// - Returns: A color simulating the given opacity, or self if semantic.
|
||||
public func opacity(_ opacity: Double) -> Self {
|
||||
guard case .rgb(let red, let green, let blue) = value else {
|
||||
guard let (red, green, blue) = rgbComponents else {
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -366,4 +422,36 @@ enum ANSIColor: UInt8, Sendable {
|
||||
var brightBackgroundCode: UInt8 {
|
||||
100 + rawValue
|
||||
}
|
||||
|
||||
// MARK: - xterm Standard RGB Values
|
||||
|
||||
/// The standard RGB values for this ANSI color (xterm defaults).
|
||||
var rgbValues: (red: UInt8, green: UInt8, blue: UInt8) {
|
||||
switch self {
|
||||
case .black: return (0, 0, 0)
|
||||
case .red: return (205, 0, 0)
|
||||
case .green: return (0, 205, 0)
|
||||
case .yellow: return (205, 205, 0)
|
||||
case .blue: return (0, 0, 238)
|
||||
case .magenta: return (205, 0, 205)
|
||||
case .cyan: return (0, 205, 205)
|
||||
case .white: return (229, 229, 229)
|
||||
case .default: return (229, 229, 229)
|
||||
}
|
||||
}
|
||||
|
||||
/// The bright RGB values for this ANSI color (xterm defaults).
|
||||
var brightRGBValues: (red: UInt8, green: UInt8, blue: UInt8) {
|
||||
switch self {
|
||||
case .black: return (127, 127, 127)
|
||||
case .red: return (255, 0, 0)
|
||||
case .green: return (0, 255, 0)
|
||||
case .yellow: return (255, 255, 0)
|
||||
case .blue: return (92, 92, 255)
|
||||
case .magenta: return (255, 0, 255)
|
||||
case .cyan: return (0, 255, 255)
|
||||
case .white: return (255, 255, 255)
|
||||
case .default: return (255, 255, 255)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,20 @@
|
||||
///
|
||||
/// Inspired by terminals like the IBM 3278 and Wyse 50.
|
||||
/// Uses a dark background with subtle amber/orange tint.
|
||||
public struct AmberPalette: Palette {
|
||||
public struct AmberPalette: BlockPalette {
|
||||
public let id = "amber"
|
||||
public let name = "Amber"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x0A0706) // App background (darkest)
|
||||
public let containerBodyBackground = Color.hex(0x251710) // Container content background
|
||||
public let containerCapBackground = Color.hex(0x1E110E) // Container header/footer background
|
||||
// Background
|
||||
public let background = Color.hex(0x0A0706)
|
||||
|
||||
// Amber text hierarchy (matching Spotnik)
|
||||
public let foreground = Color.hex(0xFFAA00) // Bright amber - primary text
|
||||
public let foregroundSecondary = Color.hex(0xCC8800) // Medium amber - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x8F6600) // Dim amber - tertiary/muted text
|
||||
public let foregroundPlaceholder = Color.hex(0x664D00) // Faint amber - placeholder text
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent = Color.hex(0xFFCC33) // Lighter amber for highlights
|
||||
public let accentSecondary = Color.hex(0xCC9900) // Darker accent
|
||||
|
||||
// Semantic colors (stay in amber family)
|
||||
public let success = Color.hex(0xFFCC00)
|
||||
@@ -39,16 +35,15 @@ public struct AmberPalette: Palette {
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x191613)
|
||||
public let appHeaderBackground = Color.hex(0x1E110E) // Same as cap
|
||||
public let overlayBackground = Color.hex(0x0A0706) // Dimming overlay
|
||||
public var buttonBackground: Color { Color.hex(0x3A2A1D) } // Lighter amber for buttons
|
||||
public let appHeaderBackground = Color.hex(0x1E110E)
|
||||
public let overlayBackground = Color.hex(0x0A0706)
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == AmberPalette {
|
||||
extension BlockPalette where Self == AmberPalette {
|
||||
/// Amber terminal palette.
|
||||
public static var amber: AmberPalette { AmberPalette() }
|
||||
}
|
||||
|
||||
@@ -9,24 +9,20 @@
|
||||
/// Inspired by vintage vacuum fluorescent displays (VFDs) found in
|
||||
/// audio equipment, cash registers, and instrument panels. Uses the
|
||||
/// characteristic bright cyan-blue glow on a near-black background.
|
||||
public struct BluePalette: Palette {
|
||||
public struct BluePalette: BlockPalette {
|
||||
public let id = "blue"
|
||||
public let name = "Blue"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x060708) // App background (darkest)
|
||||
public let containerBodyBackground = Color.hex(0x0E1825) // Container content background
|
||||
public let containerCapBackground = Color.hex(0x0A121C) // Container header/footer background
|
||||
// Background
|
||||
public let background = Color.hex(0x060708)
|
||||
|
||||
// Blue text hierarchy
|
||||
public let foreground = Color.hex(0x00AAFF) // Bright VFD blue - primary text
|
||||
public let foregroundSecondary = Color.hex(0x0088CC) // Medium blue - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x006699) // Dim blue - tertiary/muted text
|
||||
public let foregroundPlaceholder = Color.hex(0x004D73) // Faint blue - placeholder text
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent = Color.hex(0x33BBFF) // Lighter blue for highlights
|
||||
public let accentSecondary = Color.hex(0x0099DD) // Darker accent
|
||||
|
||||
// Semantic colors (stay in blue family)
|
||||
public let success = Color.hex(0x33CCFF) // Cyan-blue
|
||||
@@ -38,17 +34,16 @@ public struct BluePalette: Palette {
|
||||
public let border = Color.hex(0x2D4A5A) // Subtle blue border
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x0F1822) // Dark blue for status bar
|
||||
public let appHeaderBackground = Color.hex(0x0A121C) // Same as cap
|
||||
public let overlayBackground = Color.hex(0x060708) // Dimming overlay
|
||||
public var buttonBackground: Color { Color.hex(0x14304A) } // Lighter blue for buttons
|
||||
public let statusBarBackground = Color.hex(0x0F1822)
|
||||
public let appHeaderBackground = Color.hex(0x0A121C)
|
||||
public let overlayBackground = Color.hex(0x060708)
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == BluePalette {
|
||||
extension BlockPalette where Self == BluePalette {
|
||||
/// Blue VFD terminal palette.
|
||||
public static var blue: BluePalette { BluePalette() }
|
||||
}
|
||||
|
||||
@@ -9,24 +9,20 @@
|
||||
///
|
||||
/// Inspired by early CRT monitors like the IBM 5151 and Apple II.
|
||||
/// Uses a dark background with subtle green tint.
|
||||
public struct GreenPalette: Palette {
|
||||
public struct GreenPalette: BlockPalette {
|
||||
public let id = "green"
|
||||
public let name = "Green"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x060A07) // App background (darkest)
|
||||
public let containerBodyBackground = Color.hex(0x0E271C) // Container content background
|
||||
public let containerCapBackground = Color.hex(0x0A1B13) // Container header/footer background
|
||||
// Background
|
||||
public let background = Color.hex(0x060A07)
|
||||
|
||||
// Green text hierarchy
|
||||
public let foreground = Color.hex(0x33FF33) // Bright green - primary text
|
||||
public let foregroundSecondary = Color.hex(0x27C227) // Medium green - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x1F8F1F) // Dim green - tertiary/muted text
|
||||
public let foregroundPlaceholder = Color.hex(0x165A16) // Faint green - placeholder text
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent = Color.hex(0x66FF66) // Lighter green for highlights
|
||||
public let accentSecondary = Color.hex(0x00CC00) // Darker accent
|
||||
|
||||
// Semantic colors (stay in green family)
|
||||
public let success = Color.hex(0x33FF33)
|
||||
@@ -38,17 +34,16 @@ public struct GreenPalette: Palette {
|
||||
public let border = Color.hex(0x2D5A2D) // Subtle green border
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x0F2215) // Dark green for status bar
|
||||
public let appHeaderBackground = Color.hex(0x0A1B13) // Same as cap
|
||||
public let overlayBackground = Color.hex(0x060A07) // Dimming overlay
|
||||
public var buttonBackground: Color { Color.hex(0x145523) } // Lighter green for buttons
|
||||
public let statusBarBackground = Color.hex(0x0F2215)
|
||||
public let appHeaderBackground = Color.hex(0x0A1B13)
|
||||
public let overlayBackground = Color.hex(0x060A07)
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == GreenPalette {
|
||||
extension BlockPalette where Self == GreenPalette {
|
||||
/// The default palette (green).
|
||||
public static var `default`: GreenPalette { GreenPalette() }
|
||||
/// Green terminal palette.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// NCursesPalette.swift
|
||||
// TUIkit
|
||||
//
|
||||
// Classic ncurses-style palette.
|
||||
//
|
||||
|
||||
/// Classic ncurses-style palette.
|
||||
///
|
||||
/// Traditional terminal colors as used in ncurses applications
|
||||
/// like htop, mc (Midnight Commander), and vim.
|
||||
public struct NCursesPalette: Palette {
|
||||
public let id = "ncurses"
|
||||
public let name = "ncurses"
|
||||
|
||||
// Standard terminal black background
|
||||
public let background = Color.black
|
||||
public let containerBodyBackground = Color.blue
|
||||
public let containerCapBackground = Color.brightBlack
|
||||
public let foreground = Color.white
|
||||
public let foregroundSecondary = Color.brightWhite
|
||||
public let foregroundTertiary = Color.brightBlack
|
||||
public let foregroundPlaceholder = Color.brightBlack
|
||||
public let accent = Color.cyan
|
||||
public let accentSecondary = Color.brightCyan
|
||||
public let success = Color.green
|
||||
public let warning = Color.yellow
|
||||
public let error = Color.red
|
||||
public let info = Color.cyan
|
||||
public let border = Color.white
|
||||
public let statusBarBackground = Color.blue
|
||||
public let appHeaderBackground = Color.brightBlack
|
||||
public let overlayBackground = Color.black
|
||||
public var buttonBackground: Color { Color.brightBlue }
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == NCursesPalette {
|
||||
/// Classic ncurses palette.
|
||||
public static var ncurses: NCursesPalette { NCursesPalette() }
|
||||
}
|
||||
@@ -9,24 +9,20 @@
|
||||
///
|
||||
/// Less common but used in some military and specialized applications.
|
||||
/// Night-vision friendly with reduced eye strain in dark environments.
|
||||
public struct RedPalette: Palette {
|
||||
public struct RedPalette: BlockPalette {
|
||||
public let id = "red"
|
||||
public let name = "Red"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x0A0606) // App background (darkest)
|
||||
public let containerBodyBackground = Color.hex(0x281112) // Container content background
|
||||
public let containerCapBackground = Color.hex(0x1E0F10) // Container header/footer background
|
||||
// Background
|
||||
public let background = Color.hex(0x0A0606)
|
||||
|
||||
// Red text hierarchy
|
||||
public let foreground = Color.hex(0xFF4444) // Bright red - primary text
|
||||
public let foregroundSecondary = Color.hex(0xCC3333) // Medium red - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x8F2222) // Dim red - tertiary/muted text
|
||||
public let foregroundPlaceholder = Color.hex(0x661616) // Faint red - placeholder text
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent = Color.hex(0xFF6666) // Lighter red for highlights
|
||||
public let accentSecondary = Color.hex(0xCC4444) // Darker accent
|
||||
|
||||
// Semantic colors (stay in red family)
|
||||
public let success = Color.hex(0xFF8080) // Light red (success in red theme)
|
||||
@@ -39,16 +35,15 @@ public struct RedPalette: Palette {
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x191313)
|
||||
public let appHeaderBackground = Color.hex(0x1E0F10) // Same as cap
|
||||
public let overlayBackground = Color.hex(0x0A0606) // Dimming overlay
|
||||
public var buttonBackground: Color { Color.hex(0x3A1F22) } // Lighter red for buttons
|
||||
public let appHeaderBackground = Color.hex(0x1E0F10)
|
||||
public let overlayBackground = Color.hex(0x0A0606)
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == RedPalette {
|
||||
extension BlockPalette where Self == RedPalette {
|
||||
/// Red terminal palette.
|
||||
public static var red: RedPalette { RedPalette() }
|
||||
}
|
||||
|
||||
@@ -9,27 +9,23 @@
|
||||
/// 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: Palette {
|
||||
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 hierarchy
|
||||
// Background
|
||||
public let background: Color
|
||||
public let containerBodyBackground: Color
|
||||
public let containerCapBackground: Color
|
||||
|
||||
// Violet text hierarchy
|
||||
public let foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
public let foregroundPlaceholder: Color
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent: Color
|
||||
public let accentSecondary: Color
|
||||
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
@@ -44,25 +40,20 @@ public struct VioletPalette: Palette {
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
public let buttonBackground: Color
|
||||
|
||||
public init() {
|
||||
let hue = Self.baseHue
|
||||
|
||||
// Backgrounds: very dark, subtly tinted
|
||||
// Background: very dark, subtly tinted
|
||||
self.background = Color.hsl(hue, 30, 3)
|
||||
self.containerBodyBackground = Color.hsl(hue, 40, 10)
|
||||
self.containerCapBackground = Color.hsl(hue, 35, 7)
|
||||
|
||||
// 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)
|
||||
self.foregroundPlaceholder = Color.hsl(hue, 50, 28)
|
||||
|
||||
// Accents: lighter/brighter variant
|
||||
// Accent: lighter/brighter variant
|
||||
self.accent = Color.hsl(hue, 85, 78)
|
||||
self.accentSecondary = Color.hsl(hue, 75, 50)
|
||||
|
||||
// Semantic: hue-shifted from base
|
||||
self.success = Color.hsl(Self.wrapHue(hue + 120), 70, 65)
|
||||
@@ -75,9 +66,8 @@ public struct VioletPalette: Palette {
|
||||
|
||||
// Additional backgrounds
|
||||
self.statusBarBackground = Color.hsl(hue, 35, 8)
|
||||
self.appHeaderBackground = Color.hsl(hue, 35, 7) // Same as cap
|
||||
self.overlayBackground = Color.hsl(hue, 30, 3) // Same as background
|
||||
self.buttonBackground = Color.hsl(hue, 45, 15)
|
||||
self.appHeaderBackground = Color.hsl(hue, 35, 7)
|
||||
self.overlayBackground = Color.hsl(hue, 30, 3)
|
||||
}
|
||||
|
||||
/// Wraps a hue value to the 0–360 range.
|
||||
@@ -90,7 +80,7 @@ public struct VioletPalette: Palette {
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == VioletPalette {
|
||||
extension BlockPalette where Self == VioletPalette {
|
||||
/// Violet terminal palette.
|
||||
public static var violet: VioletPalette { VioletPalette() }
|
||||
}
|
||||
|
||||
@@ -9,24 +9,20 @@
|
||||
///
|
||||
/// Inspired by terminals like the DEC VT100 and VT220.
|
||||
/// Uses a dark background with subtle cool/blue tint.
|
||||
public struct WhitePalette: Palette {
|
||||
public struct WhitePalette: BlockPalette {
|
||||
public let id = "white"
|
||||
public let name = "White"
|
||||
|
||||
// Background hierarchy
|
||||
public let background = Color.hex(0x06070A) // App background (darkest)
|
||||
public let containerBodyBackground = Color.hex(0x111A2A) // Container content background
|
||||
public let containerCapBackground = Color.hex(0x0D131D) // Container header/footer background
|
||||
// Background
|
||||
public let background = Color.hex(0x06070A)
|
||||
|
||||
// White/gray text hierarchy
|
||||
public let foreground = Color.hex(0xE8E8E8) // Bright white - primary text
|
||||
public let foregroundSecondary = Color.hex(0xB0B0B0) // Medium gray - secondary text
|
||||
public let foregroundTertiary = Color.hex(0x787878) // Dim gray - tertiary/muted text
|
||||
public let foregroundPlaceholder = Color.hex(0x505050) // Faint gray - placeholder text
|
||||
|
||||
// Accent colors
|
||||
// Accent
|
||||
public let accent = Color.hex(0xFFFFFF) // Pure white for highlights
|
||||
public let accentSecondary = Color.hex(0xC0C0C0) // Light gray accent
|
||||
|
||||
// Semantic colors (subtle tints)
|
||||
public let success = Color.hex(0xC0FFC0) // Slight green tint
|
||||
@@ -39,16 +35,15 @@ public struct WhitePalette: Palette {
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x131619)
|
||||
public let appHeaderBackground = Color.hex(0x0D131D) // Same as cap
|
||||
public let overlayBackground = Color.hex(0x06070A) // Dimming overlay
|
||||
public var buttonBackground: Color { Color.hex(0x1D2535) } // Lighter gray for buttons
|
||||
public let appHeaderBackground = Color.hex(0x0D131D)
|
||||
public let overlayBackground = Color.hex(0x06070A)
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
extension Palette where Self == WhitePalette {
|
||||
extension BlockPalette where Self == WhitePalette {
|
||||
/// White terminal palette.
|
||||
public static var white: WhitePalette { WhitePalette() }
|
||||
}
|
||||
|
||||
@@ -18,24 +18,24 @@
|
||||
/// Text("Hello").foregroundColor(.palette.accent)
|
||||
/// ```
|
||||
enum SemanticColor: String, Sendable, Equatable {
|
||||
// Background
|
||||
// Background (Palette)
|
||||
case background
|
||||
case containerBodyBackground
|
||||
case containerCapBackground
|
||||
case buttonBackground
|
||||
case statusBarBackground
|
||||
case appHeaderBackground
|
||||
case overlayBackground
|
||||
|
||||
// Background (BlockPalette)
|
||||
case surfaceBackground
|
||||
case surfaceHeaderBackground
|
||||
case elevatedBackground
|
||||
|
||||
// Foreground
|
||||
case foreground
|
||||
case foregroundSecondary
|
||||
case foregroundTertiary
|
||||
case foregroundPlaceholder
|
||||
|
||||
// Accent
|
||||
case accent
|
||||
case accentSecondary
|
||||
|
||||
// Status
|
||||
case success
|
||||
@@ -48,28 +48,37 @@ enum SemanticColor: String, Sendable, Equatable {
|
||||
|
||||
/// Resolves this token to a concrete color using the given palette.
|
||||
///
|
||||
/// For ``BlockPalette``-specific tokens (`surfaceBackground`,
|
||||
/// `surfaceHeaderBackground`, `elevatedBackground`), the palette is
|
||||
/// cast to ``BlockPalette``. If the cast fails, `background` is used
|
||||
/// as fallback.
|
||||
///
|
||||
/// - Parameter palette: The palette to read from.
|
||||
/// - Returns: The concrete ``Color`` from the palette.
|
||||
func resolve(with palette: any Palette) -> Color {
|
||||
switch self {
|
||||
// Palette properties
|
||||
case .background: palette.background
|
||||
case .containerBodyBackground: palette.containerBodyBackground
|
||||
case .containerCapBackground: palette.containerCapBackground
|
||||
case .buttonBackground: palette.buttonBackground
|
||||
case .statusBarBackground: palette.statusBarBackground
|
||||
case .appHeaderBackground: palette.appHeaderBackground
|
||||
case .overlayBackground: palette.overlayBackground
|
||||
case .foreground: palette.foreground
|
||||
case .foregroundSecondary: palette.foregroundSecondary
|
||||
case .foregroundTertiary: palette.foregroundTertiary
|
||||
case .foregroundPlaceholder: palette.foregroundPlaceholder
|
||||
case .accent: palette.accent
|
||||
case .accentSecondary: palette.accentSecondary
|
||||
case .success: palette.success
|
||||
case .warning: palette.warning
|
||||
case .error: palette.error
|
||||
case .info: palette.info
|
||||
case .border: palette.border
|
||||
|
||||
// BlockPalette properties (fallback to background)
|
||||
case .surfaceBackground:
|
||||
(palette as? any BlockPalette)?.surfaceBackground ?? palette.background
|
||||
case .surfaceHeaderBackground:
|
||||
(palette as? any BlockPalette)?.surfaceHeaderBackground ?? palette.background
|
||||
case .elevatedBackground:
|
||||
(palette as? any BlockPalette)?.elevatedBackground ?? palette.background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ import Foundation
|
||||
///
|
||||
/// Conforms to ``Cyclable`` so it can be managed by a ``ThemeManager``.
|
||||
///
|
||||
/// For block-appearance-specific backgrounds (surfaces, elevated elements),
|
||||
/// see ``BlockPalette``.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```swift
|
||||
@@ -32,19 +35,10 @@ public protocol Palette: Cyclable {
|
||||
/// The app background color (darkest).
|
||||
var background: Color { get }
|
||||
|
||||
/// Container body/content background.
|
||||
var containerBodyBackground: Color { get }
|
||||
|
||||
/// Container header/footer background (the "cap" of a container).
|
||||
var containerCapBackground: Color { get }
|
||||
|
||||
/// Button background (slightly lighter than containerCapBackground).
|
||||
var buttonBackground: Color { get }
|
||||
|
||||
/// Status bar background.
|
||||
var statusBarBackground: Color { get }
|
||||
|
||||
/// App header background (for future use).
|
||||
/// App header background.
|
||||
var appHeaderBackground: Color { get }
|
||||
|
||||
/// Dimming overlay background for alerts and dialogs.
|
||||
@@ -61,17 +55,11 @@ public protocol Palette: Cyclable {
|
||||
/// Tertiary text color (even less prominent).
|
||||
var foregroundTertiary: Color { get }
|
||||
|
||||
/// Placeholder text color (weakest foreground, e.g. for empty input fields).
|
||||
var foregroundPlaceholder: Color { get }
|
||||
|
||||
// MARK: - Accent Colors
|
||||
|
||||
/// Primary accent color for interactive elements.
|
||||
var accent: Color { get }
|
||||
|
||||
/// Secondary accent color.
|
||||
var accentSecondary: Color { get }
|
||||
|
||||
// MARK: - Semantic Colors
|
||||
|
||||
/// Color for success states.
|
||||
@@ -97,23 +85,81 @@ public protocol Palette: Cyclable {
|
||||
extension Palette {
|
||||
// MARK: - Background Defaults
|
||||
|
||||
public var containerBodyBackground: Color { background }
|
||||
public var containerCapBackground: Color { background }
|
||||
public var buttonBackground: Color { containerCapBackground }
|
||||
public var statusBarBackground: Color { background }
|
||||
public var appHeaderBackground: Color { containerCapBackground }
|
||||
public var appHeaderBackground: Color { background }
|
||||
public var overlayBackground: Color { background }
|
||||
|
||||
// MARK: - Foreground Defaults
|
||||
|
||||
public var foregroundSecondary: Color { foreground }
|
||||
public var foregroundTertiary: Color { foreground }
|
||||
public var foregroundPlaceholder: Color { foregroundTertiary }
|
||||
}
|
||||
|
||||
// MARK: - Accent Defaults
|
||||
// MARK: - BlockPalette Protocol
|
||||
|
||||
public var accentSecondary: Color { accent }
|
||||
/// A palette with additional background colors for block-style appearances.
|
||||
///
|
||||
/// Block appearances use solid background fills to visually separate containers,
|
||||
/// headers, and interactive elements. `BlockPalette` extends ``Palette`` with
|
||||
/// three surface-level backgrounds that create this visual hierarchy.
|
||||
///
|
||||
/// All three properties provide computed defaults based on ``Palette/background``
|
||||
/// using ``Color/lighter(by:)``, so conforming types don't need to define them
|
||||
/// explicitly unless custom values are desired.
|
||||
///
|
||||
/// # Default Hierarchy
|
||||
///
|
||||
/// ```
|
||||
/// background (darkest)
|
||||
/// └── surfaceHeaderBackground (background.lighter(by: 0.05))
|
||||
/// └── surfaceBackground (background.lighter(by: 0.08))
|
||||
/// └── elevatedBackground (surfaceHeaderBackground.lighter(by: 0.05))
|
||||
/// ```
|
||||
public protocol BlockPalette: Palette {
|
||||
/// Container body/content background.
|
||||
///
|
||||
/// Used for the main content area of containers, menus, and bordered regions
|
||||
/// in block appearance mode.
|
||||
var surfaceBackground: Color { get }
|
||||
|
||||
/// Container header/footer background.
|
||||
///
|
||||
/// Used for the "cap" area of containers (title bars, footers) and menu
|
||||
/// headers in block appearance mode.
|
||||
var surfaceHeaderBackground: Color { get }
|
||||
|
||||
/// Elevated element background (buttons, interactive surfaces).
|
||||
///
|
||||
/// Used for elements that sit visually "above" the surface, such as
|
||||
/// buttons in block appearance mode.
|
||||
var elevatedBackground: Color { get }
|
||||
}
|
||||
|
||||
// MARK: - Default BlockPalette Implementation
|
||||
|
||||
extension BlockPalette {
|
||||
public var surfaceBackground: Color { background.lighter(by: 0.08) }
|
||||
public var surfaceHeaderBackground: Color { background.lighter(by: 0.05) }
|
||||
public var elevatedBackground: Color { surfaceHeaderBackground.lighter(by: 0.05) }
|
||||
}
|
||||
|
||||
// MARK: - BlockPalette Convenience Accessors
|
||||
|
||||
extension Palette {
|
||||
/// The surface background if this palette is a ``BlockPalette``, otherwise ``background``.
|
||||
var blockSurfaceBackground: Color {
|
||||
(self as? any BlockPalette)?.surfaceBackground ?? background
|
||||
}
|
||||
|
||||
/// The surface header background if this palette is a ``BlockPalette``, otherwise ``background``.
|
||||
var blockSurfaceHeaderBackground: Color {
|
||||
(self as? any BlockPalette)?.surfaceHeaderBackground ?? background
|
||||
}
|
||||
|
||||
/// The elevated background if this palette is a ``BlockPalette``, otherwise ``background``.
|
||||
var blockElevatedBackground: Color {
|
||||
(self as? any BlockPalette)?.elevatedBackground ?? background
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Palette Environment Key
|
||||
@@ -153,7 +199,7 @@ extension EnvironmentValues {
|
||||
struct PaletteRegistry {
|
||||
/// All available palettes in cycling order.
|
||||
///
|
||||
/// Order: Green → Amber → Red → Violet → Blue → White → NCurses
|
||||
/// Order: Green → Amber → Red → Violet → Blue → White
|
||||
static let all: [any Palette] = [
|
||||
GreenPalette(),
|
||||
AmberPalette(),
|
||||
@@ -161,7 +207,6 @@ struct PaletteRegistry {
|
||||
VioletPalette(),
|
||||
BluePalette(),
|
||||
WhitePalette(),
|
||||
NCursesPalette(),
|
||||
]
|
||||
|
||||
/// Finds a palette by ID.
|
||||
|
||||
@@ -103,7 +103,7 @@ Use `Color.palette.*` — these return semantic tokens:
|
||||
```swift
|
||||
Text("Hello")
|
||||
.foregroundColor(.palette.accent) // resolves to palette's accent color
|
||||
.background(.palette.containerBodyBackground)
|
||||
.background(.palette.surfaceBackground)
|
||||
```
|
||||
|
||||
Available semantic tokens include:
|
||||
@@ -112,10 +112,12 @@ Available semantic tokens include:
|
||||
|-------|-------------|
|
||||
| `.palette.foreground` | Primary text |
|
||||
| `.palette.foregroundSecondary` | Secondary / dimmed text |
|
||||
| `.palette.foregroundTertiary` | Disabled / muted text |
|
||||
| `.palette.accent` | Highlighted elements, titles |
|
||||
| `.palette.border` | Container borders |
|
||||
| `.palette.containerBodyBackground` | Container body / content area background |
|
||||
| `.palette.containerCapBackground` | Container header / footer background |
|
||||
| `.palette.surfaceBackground` | Container body / content area (block appearance) |
|
||||
| `.palette.surfaceHeaderBackground` | Container header / footer (block appearance) |
|
||||
| `.palette.elevatedBackground` | Buttons, interactive surfaces (block appearance) |
|
||||
| `.palette.success` / `.warning` / `.error` / `.info` | Status indicators |
|
||||
|
||||
### In renderToBuffer (with RenderContext)
|
||||
|
||||
@@ -4,14 +4,14 @@ A visual reference for all built-in color palettes with their exact color values
|
||||
|
||||
## Overview
|
||||
|
||||
TUIkit ships with **7 palettes** — 5 handcrafted phosphor themes, 1 classic ncurses theme, and 1 violet theme generated from a single base hue. Each palette defines semantic color tokens that the framework resolves at render time.
|
||||
TUIkit ships with **6 palettes** — 5 handcrafted phosphor themes and 1 violet theme generated from a single base hue. Each palette defines semantic color tokens that the framework resolves at render time.
|
||||
|
||||
Users access palette colors via `Color.palette.*`:
|
||||
|
||||
```swift
|
||||
Text("Hello")
|
||||
.foregroundColor(.palette.accent)
|
||||
.background(.palette.containerBodyBackground)
|
||||
.background(.palette.surfaceBackground)
|
||||
```
|
||||
|
||||
Cycle through palettes at runtime by pressing `t` (default binding), or set a specific palette programmatically:
|
||||
@@ -20,19 +20,39 @@ Cycle through palettes at runtime by pressing `t` (default binding), or set a sp
|
||||
environment.paletteManager.setCurrent(AmberPalette())
|
||||
```
|
||||
|
||||
## Color Token Categories
|
||||
## Protocol Hierarchy
|
||||
|
||||
Every palette must define colors for these 6 categories:
|
||||
TUIkit uses a two-level palette protocol:
|
||||
|
||||
- **``Palette``** — 13 essential color tokens (8 required, 5 with defaults)
|
||||
- **``BlockPalette``** — Extends `Palette` with 3 surface backgrounds for block-style appearances
|
||||
|
||||
```
|
||||
Palette (13 properties)
|
||||
├── Required: background, foreground, accent, border,
|
||||
│ success, warning, error, info
|
||||
├── Defaults: statusBarBackground, appHeaderBackground, overlayBackground,
|
||||
│ foregroundSecondary, foregroundTertiary
|
||||
└── BlockPalette: Palette (+3 computed properties)
|
||||
├── surfaceBackground (background.lighter(by: 0.08))
|
||||
├── surfaceHeaderBackground (background.lighter(by: 0.05))
|
||||
└── elevatedBackground (surfaceHeaderBackground.lighter(by: 0.05))
|
||||
```
|
||||
|
||||
All 6 built-in palettes conform to ``BlockPalette``. Custom palettes can conform to just ``Palette`` if they don't need block-style surfaces.
|
||||
|
||||
## Color Token Categories
|
||||
|
||||
| Category | Tokens | Purpose |
|
||||
|----------|--------|---------|
|
||||
| **Background** | `background`, `containerBodyBackground`, `containerCapBackground`, `buttonBackground`, `statusBarBackground`, `appHeaderBackground`, `overlayBackground` | App background, container body/cap, buttons, status bar, overlays |
|
||||
| **Background** | `background`, `statusBarBackground`, `appHeaderBackground`, `overlayBackground` | App background, status bar, overlays |
|
||||
| **Block Surfaces** | `surfaceBackground`, `surfaceHeaderBackground`, `elevatedBackground` | Container body, headers, buttons (block appearance only) |
|
||||
| **Foreground** | `foreground`, `foregroundSecondary`, `foregroundTertiary` | Primary, secondary, and tertiary text |
|
||||
| **Accent** | `accent`, `accentSecondary` | Interactive elements, highlights |
|
||||
| **Accent** | `accent` | Interactive elements, highlights |
|
||||
| **Semantic** | `success`, `warning`, `error`, `info` | Status indicators |
|
||||
| **UI Elements** | `border` | Borders |
|
||||
|
||||
Only 10 tokens are required — the remaining 8 have sensible defaults derived from the required ones. See <doc:ThemingGuide> for details on creating custom palettes.
|
||||
Only 8 tokens are required — the remaining have sensible defaults. See <doc:ThemingGuide> for details on creating custom palettes.
|
||||
|
||||
## Green (Default)
|
||||
|
||||
@@ -45,13 +65,10 @@ Inspired by P1 phosphor CRT monitors (IBM 5151, Apple II). This is the default p
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `background` | `#060A07` | (6, 10, 7) | Near-black with green tint |
|
||||
| `containerBodyBackground` | `#0E271C` | (14, 39, 28) | Container content area |
|
||||
| `containerCapBackground` | `#0A1B13` | (10, 27, 19) | Container header/footer |
|
||||
| `foreground` | `#33FF33` | (51, 255, 51) | Classic phosphor green |
|
||||
| `foregroundSecondary` | `#27C227` | (39, 194, 39) | Dimmer green |
|
||||
| `foregroundTertiary` | `#1F8F1F` | (31, 143, 31) | Subtle green |
|
||||
| `accent` | `#66FF66` | (102, 255, 102) | Bright green highlight |
|
||||
| `accentSecondary` | `#00CC00` | (0, 204, 0) | Medium green |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
@@ -68,7 +85,14 @@ Inspired by P1 phosphor CRT monitors (IBM 5151, Apple II). This is the default p
|
||||
|-------|-----|-----|
|
||||
| `border` | `#2D5A2D` | (45, 90, 45) |
|
||||
| `statusBarBackground` | `#0F2215` | (15, 34, 21) |
|
||||
| `buttonBackground` | `#145523` | (20, 85, 35) |
|
||||
|
||||
### Block Surfaces (computed from background)
|
||||
|
||||
| Token | Description |
|
||||
|-------|-------------|
|
||||
| `surfaceBackground` | Container body area |
|
||||
| `surfaceHeaderBackground` | Container header/footer |
|
||||
| `elevatedBackground` | Buttons, interactive surfaces |
|
||||
|
||||
## Amber
|
||||
|
||||
@@ -81,13 +105,10 @@ Inspired by P3 phosphor CRT monitors (IBM 3278, Wyse 50). Warm amber tones remin
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `background` | `#0A0706` | (10, 7, 6) | Near-black with warm tint |
|
||||
| `containerBodyBackground` | `#251710` | (37, 23, 16) | Container content area |
|
||||
| `containerCapBackground` | `#1E110E` | (30, 17, 14) | Container header/footer |
|
||||
| `foreground` | `#FFAA00` | (255, 170, 0) | Classic amber phosphor |
|
||||
| `foregroundSecondary` | `#CC8800` | (204, 136, 0) | Dimmer amber |
|
||||
| `foregroundTertiary` | `#8F6600` | (143, 102, 0) | Subtle amber |
|
||||
| `accent` | `#FFCC33` | (255, 204, 51) | Bright amber highlight |
|
||||
| `accentSecondary` | `#CC9900` | (204, 153, 0) | Medium amber |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
@@ -104,7 +125,6 @@ Inspired by P3 phosphor CRT monitors (IBM 3278, Wyse 50). Warm amber tones remin
|
||||
|-------|-----|-----|
|
||||
| `border` | `#5A4A2D` | (90, 74, 45) |
|
||||
| `statusBarBackground` | `#191613` | (25, 22, 19) |
|
||||
| `buttonBackground` | `#3A2A1D` | (58, 42, 29) |
|
||||
|
||||
## White
|
||||
|
||||
@@ -117,13 +137,10 @@ Inspired by P4 phosphor CRT monitors (DEC VT100, VT220). Clean monochrome with c
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `background` | `#06070A` | (6, 7, 10) | Near-black with blue tint |
|
||||
| `containerBodyBackground` | `#111A2A` | (17, 26, 42) | Container content area |
|
||||
| `containerCapBackground` | `#0D131D` | (13, 19, 29) | Container header/footer |
|
||||
| `foreground` | `#E8E8E8` | (232, 232, 232) | Off-white text |
|
||||
| `foregroundSecondary` | `#B0B0B0` | (176, 176, 176) | Light gray |
|
||||
| `foregroundTertiary` | `#787878` | (120, 120, 120) | Medium gray |
|
||||
| `accent` | `#FFFFFF` | (255, 255, 255) | Pure white highlight |
|
||||
| `accentSecondary` | `#C0C0C0` | (192, 192, 192) | Silver |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
@@ -140,7 +157,6 @@ Inspired by P4 phosphor CRT monitors (DEC VT100, VT220). Clean monochrome with c
|
||||
|-------|-----|-----|
|
||||
| `border` | `#484848` | (72, 72, 72) |
|
||||
| `statusBarBackground` | `#131619` | (19, 22, 25) |
|
||||
| `buttonBackground` | `#1D2535` | (29, 37, 53) |
|
||||
|
||||
## Red
|
||||
|
||||
@@ -153,13 +169,10 @@ Inspired by military and night-vision-friendly displays. Preserves scotopic (nig
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `background` | `#0A0606` | (10, 6, 6) | Near-black with red tint |
|
||||
| `containerBodyBackground` | `#281112` | (40, 17, 18) | Container content area |
|
||||
| `containerCapBackground` | `#1E0F10` | (30, 15, 16) | Container header/footer |
|
||||
| `foreground` | `#FF4444` | (255, 68, 68) | Bright red text |
|
||||
| `foregroundSecondary` | `#CC3333` | (204, 51, 51) | Dimmer red |
|
||||
| `foregroundTertiary` | `#8F2222` | (143, 34, 34) | Subtle red |
|
||||
| `accent` | `#FF6666` | (255, 102, 102) | Light red highlight |
|
||||
| `accentSecondary` | `#CC4444` | (204, 68, 68) | Medium red |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
@@ -176,45 +189,6 @@ Inspired by military and night-vision-friendly displays. Preserves scotopic (nig
|
||||
|-------|-----|-----|
|
||||
| `border` | `#5A2D2D` | (90, 45, 45) |
|
||||
| `statusBarBackground` | `#191313` | (25, 19, 19) |
|
||||
| `buttonBackground` | `#3A1F22` | (58, 31, 34) |
|
||||
|
||||
## ncurses
|
||||
|
||||
Classic ncurses terminal colors (htop, Midnight Commander, vim). The only palette using standard ANSI colors instead of RGB — maximum compatibility with 16-color terminals.
|
||||
|
||||
**Palette type:** ``NCursesPalette`` · **ID:** `"ncurses"`
|
||||
|
||||
### Core Colors
|
||||
|
||||
| Token | ANSI Color | Code | Description |
|
||||
|-------|-----------|------|-------------|
|
||||
| `background` | Black | 30/40 | Terminal default black |
|
||||
| `containerBodyBackground` | Blue | 34/44 | Classic ncurses panel blue |
|
||||
| `containerCapBackground` | Bright Black | 90/100 | Dark gray |
|
||||
| `foreground` | White | 37/47 | Standard white |
|
||||
| `foregroundSecondary` | Bright White | 97/107 | Brighter white |
|
||||
| `foregroundTertiary` | Bright Black | 90/100 | Gray (dimmed) |
|
||||
| `accent` | Cyan | 36/46 | Primary interactive color |
|
||||
| `accentSecondary` | Bright Cyan | 96/106 | Highlighted interactive |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
| Token | ANSI Color | Code | Description |
|
||||
|-------|-----------|------|-------------|
|
||||
| `success` | Green | 32/42 | Standard green |
|
||||
| `warning` | Yellow | 33/43 | Standard yellow |
|
||||
| `error` | Red | 31/41 | Standard red |
|
||||
| `info` | Cyan | 36/46 | Same as accent |
|
||||
|
||||
### UI Elements
|
||||
|
||||
| Token | ANSI Color | Code |
|
||||
|-------|-----------|------|
|
||||
| `border` | White | 37/47 |
|
||||
| `statusBarBackground` | Blue | 34/44 |
|
||||
| `buttonBackground` | Bright Blue | 94/104 |
|
||||
|
||||
> Tip: Use the ncurses palette when targeting terminals with limited color support (SSH sessions, older terminal emulators, or 16-color environments).
|
||||
|
||||
## Violet
|
||||
|
||||
@@ -226,9 +200,9 @@ An algorithmically generated palette based on HSL color theory with a base hue o
|
||||
|
||||
``VioletPalette`` takes a base hue (270°) and derives all color tokens using HSL relationships:
|
||||
|
||||
- **Backgrounds** — Base hue at very low lightness (3–10%) with reduced saturation
|
||||
- **Background** — Base hue at very low lightness (3%) with reduced saturation
|
||||
- **Foregrounds** — Base hue at medium-high lightness (40–70%)
|
||||
- **Accents** — Base hue at high lightness (78%) with high saturation
|
||||
- **Accent** — Base hue at high lightness (78%) with high saturation
|
||||
- **Semantic colors** — Derived from color theory offsets:
|
||||
- `success` = base + 120° (triadic)
|
||||
- `warning` = base + 60° (analogous warm)
|
||||
@@ -240,20 +214,48 @@ An algorithmically generated palette based on HSL color theory with a base hue o
|
||||
| Token | HSL | Description |
|
||||
|-------|-----|-------------|
|
||||
| `background` | hsl(270, 30%, 3%) | Near-black with violet tint |
|
||||
| `containerBodyBackground` | hsl(270, 40%, 10%) | Container content area |
|
||||
| `containerCapBackground` | hsl(270, 35%, 7%) | Container header/footer |
|
||||
| `foreground` | hsl(270, 80%, 70%) | Light violet text |
|
||||
| `foregroundSecondary` | hsl(270, 70%, 55%) | Medium violet |
|
||||
| `foregroundTertiary` | hsl(270, 60%, 40%) | Dim violet |
|
||||
| `accent` | hsl(270, 85%, 78%) | Bright lavender |
|
||||
| `accentSecondary` | hsl(270, 75%, 50%) | Medium purple |
|
||||
| `success` | hsl(30, 70%, 65%) | Warm orange (270+120=30°) |
|
||||
| `warning` | hsl(330, 80%, 70%) | Pink (270+60=330°) |
|
||||
| `error` | hsl(90, 85%, 65%) | Lime green (270+180=90°) |
|
||||
| `info` | hsl(210, 70%, 70%) | Sky blue (270−60=210°) |
|
||||
| `border` | hsl(270, 40%, 25%) | Dark purple border |
|
||||
| `statusBarBackground` | hsl(270, 35%, 8%) | Very dark violet |
|
||||
| `buttonBackground` | hsl(270, 45%, 15%) | Dark purple |
|
||||
|
||||
## Blue
|
||||
|
||||
Inspired by vintage vacuum fluorescent displays (VFDs). The characteristic bright cyan-blue glow.
|
||||
|
||||
**Palette type:** ``BluePalette`` · **ID:** `"blue"`
|
||||
|
||||
### Core Colors
|
||||
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `background` | `#060708` | (6, 7, 8) | Near-black with blue tint |
|
||||
| `foreground` | `#00AAFF` | (0, 170, 255) | Bright VFD blue |
|
||||
| `foregroundSecondary` | `#0088CC` | (0, 136, 204) | Medium blue |
|
||||
| `foregroundTertiary` | `#006699` | (0, 102, 153) | Dim blue |
|
||||
| `accent` | `#33BBFF` | (51, 187, 255) | Lighter blue highlight |
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
| Token | Hex | RGB | Description |
|
||||
|-------|-----|-----|-------------|
|
||||
| `success` | `#33CCFF` | (51, 204, 255) | Cyan-blue |
|
||||
| `warning` | `#66CCFF` | (102, 204, 255) | Light cyan |
|
||||
| `error` | `#FF6633` | (255, 102, 51) | Orange-red contrast |
|
||||
| `info` | `#99DDFF` | (153, 221, 255) | Pale blue |
|
||||
|
||||
### UI Elements
|
||||
|
||||
| Token | Hex | RGB |
|
||||
|-------|-----|-----|
|
||||
| `border` | `#2D4A5A` | (45, 74, 90) |
|
||||
| `statusBarBackground` | `#0F1822` | (15, 24, 34) |
|
||||
|
||||
## Palette Cycling Order
|
||||
|
||||
@@ -267,7 +269,6 @@ When pressing `t` to cycle themes, palettes rotate in this order:
|
||||
| 4 | Violet | HSL-generated (hue 270°) |
|
||||
| 5 | Blue | Handcrafted (VFD) |
|
||||
| 6 | White | Handcrafted |
|
||||
| 7 | ncurses | Handcrafted (ANSI) |
|
||||
|
||||
## Color Resolution Flow
|
||||
|
||||
@@ -276,22 +277,26 @@ When you write `.foregroundColor(.palette.accent)`, TUIkit resolves the actual c
|
||||
1. **Declaration** — `Color.palette.accent` creates a `Color` with a semantic token (`.accent`)
|
||||
2. **Render pass** — The current palette is read from `context.environment.palette`
|
||||
3. **Resolution** — The semantic token maps to the palette's `accent` property
|
||||
4. **ANSI output** — The resolved RGB/ANSI color is converted to terminal escape codes
|
||||
4. **BlockPalette tokens** — For `surfaceBackground` etc., the palette is cast to ``BlockPalette``; if the cast fails, `background` is used as fallback
|
||||
5. **ANSI output** — The resolved RGB/ANSI color is converted to terminal escape codes
|
||||
|
||||
This means the same view code produces different colors depending on the active palette — no code changes needed when switching themes.
|
||||
|
||||
## Topics
|
||||
|
||||
### Palettes
|
||||
### Protocols
|
||||
|
||||
- ``Palette``
|
||||
- ``BlockPalette``
|
||||
|
||||
### Palettes
|
||||
|
||||
- ``GreenPalette``
|
||||
- ``AmberPalette``
|
||||
- ``RedPalette``
|
||||
- ``VioletPalette``
|
||||
- ``BluePalette``
|
||||
- ``WhitePalette``
|
||||
- ``NCursesPalette``
|
||||
|
||||
### Color System
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Customize the visual appearance of your TUIkit application with palettes.
|
||||
|
||||
## Overview
|
||||
|
||||
TUIkit includes a full theming system with seven built-in palettes inspired by classic CRT terminals. Palettes define semantic colors for backgrounds, foregrounds, accents, and UI elements.
|
||||
TUIkit includes a full theming system with six built-in palettes inspired by classic CRT terminals. Palettes define semantic colors for backgrounds, foregrounds, accents, and UI elements.
|
||||
|
||||
## Built-in Palettes
|
||||
|
||||
@@ -16,7 +16,6 @@ TUIkit includes a full theming system with seven built-in palettes inspired by c
|
||||
| Violet | ``VioletPalette`` | Retro sci-fi displays |
|
||||
| Blue | ``BluePalette`` | VFD displays |
|
||||
| White | ``WhitePalette`` | DEC VT100, VT220 |
|
||||
| ncurses | ``NCursesPalette`` | Classic ncurses apps |
|
||||
|
||||
## Using Palettes
|
||||
|
||||
@@ -55,7 +54,7 @@ Use ``Color/palette`` to access the current palette's colors:
|
||||
```swift
|
||||
Text("Styled text")
|
||||
.foregroundColor(.palette.foreground)
|
||||
.backgroundColor(.palette.containerBodyBackground)
|
||||
.backgroundColor(.palette.surfaceBackground)
|
||||
```
|
||||
|
||||
Or read the palette directly from the environment:
|
||||
@@ -68,7 +67,9 @@ Text("Hello").foregroundColor(palette.accent)
|
||||
|
||||
## Creating Custom Palettes
|
||||
|
||||
Implement the ``Palette`` protocol:
|
||||
### Minimal Palette (Palette protocol)
|
||||
|
||||
Implement the ``Palette`` protocol for a palette with just the essential colors:
|
||||
|
||||
```swift
|
||||
struct MyCustomPalette: Palette {
|
||||
@@ -78,20 +79,53 @@ struct MyCustomPalette: Palette {
|
||||
let background = Color.hex(0x1A1A2E)
|
||||
let foreground = Color.hex(0xE0E0E0)
|
||||
let accent = Color.hex(0x00D4FF)
|
||||
let success = Color.hex(0x00FF88)
|
||||
let warning = Color.hex(0xFFCC00)
|
||||
let error = Color.hex(0xFF4444)
|
||||
let info = Color.hex(0x44AAFF)
|
||||
let border = Color.hex(0x333355)
|
||||
|
||||
// ... implement remaining required properties
|
||||
// Many have default implementations via Palette extension
|
||||
// Optional: override defaults for statusBarBackground,
|
||||
// appHeaderBackground, overlayBackground,
|
||||
// foregroundSecondary, foregroundTertiary
|
||||
}
|
||||
```
|
||||
|
||||
### Block-Aware Palette (BlockPalette protocol)
|
||||
|
||||
For palettes that provide block-appearance surface colors, conform to ``BlockPalette``:
|
||||
|
||||
```swift
|
||||
struct MyBlockPalette: BlockPalette {
|
||||
let id = "custom-block"
|
||||
let name = "Custom Block"
|
||||
|
||||
// ... same required properties as Palette ...
|
||||
|
||||
// Optional: override computed defaults
|
||||
// var surfaceBackground: Color { ... }
|
||||
// var surfaceHeaderBackground: Color { ... }
|
||||
// var elevatedBackground: Color { ... }
|
||||
}
|
||||
```
|
||||
|
||||
The ``BlockPalette`` defaults compute surface colors from `background` using `lighter(by:)`:
|
||||
- `surfaceBackground` = `background.lighter(by: 0.08)`
|
||||
- `surfaceHeaderBackground` = `background.lighter(by: 0.05)`
|
||||
- `elevatedBackground` = `surfaceHeaderBackground.lighter(by: 0.05)`
|
||||
|
||||
## Palette Color Properties
|
||||
|
||||
The ``Palette`` protocol defines these semantic color categories:
|
||||
### Palette (Base Protocol)
|
||||
|
||||
- **Backgrounds**: `background`, `containerBodyBackground`, `containerCapBackground`, `buttonBackground`, `statusBarBackground`, `appHeaderBackground`, `overlayBackground`
|
||||
- **Backgrounds**: `background`, `statusBarBackground`, `appHeaderBackground`, `overlayBackground`
|
||||
- **Foregrounds**: `foreground`, `foregroundSecondary`, `foregroundTertiary`
|
||||
- **Accents**: `accent`, `accentSecondary`
|
||||
- **Accent**: `accent`
|
||||
- **Semantic**: `success`, `warning`, `error`, `info`
|
||||
- **UI Elements**: `border`
|
||||
|
||||
Many of these have default implementations that derive from the primary colors, so a minimal palette only needs to define a handful of values.
|
||||
### BlockPalette (extends Palette)
|
||||
|
||||
- **Surfaces**: `surfaceBackground`, `surfaceHeaderBackground`, `elevatedBackground`
|
||||
|
||||
Only 8 properties are required (`background`, `foreground`, `accent`, `border`, `success`, `warning`, `error`, `info`). All others have default implementations that derive from these.
|
||||
|
||||
@@ -114,12 +114,14 @@ struct MyApp: App {
|
||||
### Theming
|
||||
|
||||
- ``Palette``
|
||||
- ``BlockPalette``
|
||||
- ``ThemeManager``
|
||||
- ``GreenPalette``
|
||||
- ``AmberPalette``
|
||||
- ``WhitePalette``
|
||||
- ``RedPalette``
|
||||
- ``NCursesPalette``
|
||||
- ``BluePalette``
|
||||
- ``VioletPalette``
|
||||
- ``GeneratedPalette``
|
||||
|
||||
### Colors
|
||||
|
||||
@@ -240,10 +240,10 @@ extension Button: Renderable {
|
||||
textStyle.backgroundColor = currentStyle.backgroundColor?.resolve(with: palette)
|
||||
textStyle.isBold = currentStyle.isBold && !isDisabled
|
||||
|
||||
// In block appearance, add buttonBackground to button
|
||||
// In block appearance, add elevated background to button
|
||||
let isBlockAppearance = context.environment.appearance.rawId == .block
|
||||
if isBlockAppearance && textStyle.backgroundColor == nil {
|
||||
textStyle.backgroundColor = context.environment.palette.buttonBackground
|
||||
textStyle.backgroundColor = context.environment.palette.blockElevatedBackground
|
||||
}
|
||||
|
||||
let styledLabel = ANSIRenderer.render(paddedLabel, with: textStyle)
|
||||
|
||||
@@ -358,7 +358,7 @@ extension ContainerView: Renderable {
|
||||
}
|
||||
|
||||
// Body lines with theme background
|
||||
let bodyBg = context.environment.palette.containerBodyBackground
|
||||
let bodyBg = context.environment.palette.blockSurfaceBackground
|
||||
for line in bodyBuffer.lines {
|
||||
lines.append(
|
||||
BorderRenderer.standardContentLine(
|
||||
@@ -435,10 +435,10 @@ extension ContainerView: Renderable {
|
||||
var lines: [String] = []
|
||||
|
||||
// Get palette colors for block appearance
|
||||
// Header/Footer = darker background
|
||||
// Body = lighter background (containerBodyBackground)
|
||||
let headerFooterBg = palette.containerCapBackground
|
||||
let bodyBg = palette.containerBodyBackground
|
||||
// Header/Footer = darker background (surfaceHeaderBackground)
|
||||
// Body = lighter background (surfaceBackground)
|
||||
let headerFooterBg = palette.blockSurfaceHeaderBackground
|
||||
let bodyBg = palette.blockSurfaceBackground
|
||||
|
||||
let hasHeader = title != nil
|
||||
let hasFooter = footerBuffer != nil && !(footerBuffer?.isEmpty ?? true)
|
||||
|
||||
@@ -236,7 +236,7 @@ extension Menu: Renderable {
|
||||
style.isBold = true
|
||||
style.foregroundColor = selectedColor?.resolve(with: palette) ?? palette.accent
|
||||
// Use a dimmed version of the accent color for background
|
||||
style.backgroundColor = palette.containerBodyBackground
|
||||
style.backgroundColor = palette.blockSurfaceBackground
|
||||
} else {
|
||||
// Use palette foreground color if no custom itemColor is set
|
||||
style.foregroundColor = itemColor?.resolve(with: palette) ?? palette.foreground
|
||||
@@ -348,8 +348,8 @@ extension Menu: Renderable {
|
||||
var result: [String] = []
|
||||
|
||||
if isBlockStyle {
|
||||
let headerFooterBg = palette.containerCapBackground
|
||||
let bodyBg = palette.containerBodyBackground
|
||||
let headerFooterBg = palette.blockSurfaceHeaderBackground
|
||||
let bodyBg = palette.blockSurfaceBackground
|
||||
let hasHeader = dividerLineIndex != nil
|
||||
|
||||
// Top border
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// PaletteDefaultTests.swift
|
||||
// TUIkit
|
||||
//
|
||||
// Tests for Palette protocol default implementations.
|
||||
// Tests for Palette and BlockPalette protocol default implementations.
|
||||
//
|
||||
|
||||
import Testing
|
||||
@@ -24,37 +24,34 @@ private struct MinimalPalette: Palette {
|
||||
let border = Color.brightBlack
|
||||
}
|
||||
|
||||
/// Minimal block palette that inherits Palette requirements
|
||||
/// and uses computed BlockPalette defaults.
|
||||
private struct MinimalBlockPalette: BlockPalette {
|
||||
let id = "minimal-block"
|
||||
let name = "Minimal Block"
|
||||
let background = Color.hex(0x0A0A0A)
|
||||
let foreground = Color.white
|
||||
let accent = Color.cyan
|
||||
let success = Color.green
|
||||
let warning = Color.yellow
|
||||
let error = Color.red
|
||||
let info = Color.blue
|
||||
let border = Color.brightBlack
|
||||
}
|
||||
|
||||
@Suite("Palette Default Implementation Tests")
|
||||
struct PaletteDefaultTests {
|
||||
|
||||
@Test("Defaults derive containerBodyBackground from background")
|
||||
func defaultContainerBodyBackground() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.containerBodyBackground == palette.background)
|
||||
}
|
||||
|
||||
@Test("Defaults derive containerCapBackground from background")
|
||||
func defaultContainerCapBackground() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.containerCapBackground == palette.background)
|
||||
}
|
||||
|
||||
@Test("Defaults derive foregroundSecondary from foreground")
|
||||
func defaultForegroundSecondary() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.foregroundSecondary == palette.foreground)
|
||||
}
|
||||
|
||||
@Test("Defaults derive foregroundPlaceholder from foregroundTertiary")
|
||||
func defaultForegroundPlaceholder() {
|
||||
@Test("Defaults derive foregroundTertiary from foreground")
|
||||
func defaultForegroundTertiary() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.foregroundPlaceholder == palette.foregroundTertiary)
|
||||
}
|
||||
|
||||
@Test("Defaults derive accentSecondary from accent")
|
||||
func defaultAccentSecondary() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.accentSecondary == palette.accent)
|
||||
#expect(palette.foregroundTertiary == palette.foreground)
|
||||
}
|
||||
|
||||
@Test("Defaults derive statusBarBackground from background")
|
||||
@@ -63,11 +60,57 @@ struct PaletteDefaultTests {
|
||||
#expect(palette.statusBarBackground == palette.background)
|
||||
}
|
||||
|
||||
@Test("Defaults derive container and button backgrounds")
|
||||
func defaultContainerColors() {
|
||||
@Test("Defaults derive appHeaderBackground from background")
|
||||
func defaultAppHeaderBackground() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.appHeaderBackground == palette.background)
|
||||
}
|
||||
|
||||
@Test("Defaults derive overlayBackground from background")
|
||||
func defaultOverlayBackground() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.buttonBackground == palette.containerCapBackground)
|
||||
#expect(palette.appHeaderBackground == palette.containerCapBackground)
|
||||
#expect(palette.overlayBackground == palette.background)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("BlockPalette Default Implementation Tests")
|
||||
struct BlockPaletteDefaultTests {
|
||||
|
||||
@Test("BlockPalette surfaceBackground is lighter than background")
|
||||
func surfaceBackgroundIsLighter() {
|
||||
let palette = MinimalBlockPalette()
|
||||
let bgComponents = palette.background.rgbComponents!
|
||||
let surfaceComponents = palette.surfaceBackground.rgbComponents!
|
||||
let bgBrightness = Int(bgComponents.red) + Int(bgComponents.green) + Int(bgComponents.blue)
|
||||
let surfaceBrightness = Int(surfaceComponents.red) + Int(surfaceComponents.green) + Int(surfaceComponents.blue)
|
||||
#expect(surfaceBrightness > bgBrightness, "surfaceBackground should be brighter than background")
|
||||
}
|
||||
|
||||
@Test("BlockPalette surfaceHeaderBackground is lighter than background")
|
||||
func surfaceHeaderBackgroundIsLighter() {
|
||||
let palette = MinimalBlockPalette()
|
||||
let bgComponents = palette.background.rgbComponents!
|
||||
let headerComponents = palette.surfaceHeaderBackground.rgbComponents!
|
||||
let bgBrightness = Int(bgComponents.red) + Int(bgComponents.green) + Int(bgComponents.blue)
|
||||
let headerBrightness = Int(headerComponents.red) + Int(headerComponents.green) + Int(headerComponents.blue)
|
||||
#expect(headerBrightness > bgBrightness, "surfaceHeaderBackground should be brighter than background")
|
||||
}
|
||||
|
||||
@Test("BlockPalette elevatedBackground is lighter than surfaceHeaderBackground")
|
||||
func elevatedBackgroundIsLighter() {
|
||||
let palette = MinimalBlockPalette()
|
||||
let headerComponents = palette.surfaceHeaderBackground.rgbComponents!
|
||||
let elevatedComponents = palette.elevatedBackground.rgbComponents!
|
||||
let headerBrightness = Int(headerComponents.red) + Int(headerComponents.green) + Int(headerComponents.blue)
|
||||
let elevatedBrightness = Int(elevatedComponents.red) + Int(elevatedComponents.green) + Int(elevatedComponents.blue)
|
||||
#expect(elevatedBrightness > headerBrightness, "elevatedBackground should be brighter than surfaceHeaderBackground")
|
||||
}
|
||||
|
||||
@Test("Non-BlockPalette falls back to background via convenience accessors")
|
||||
func nonBlockPaletteFallback() {
|
||||
let palette = MinimalPalette()
|
||||
#expect(palette.blockSurfaceBackground == palette.background)
|
||||
#expect(palette.blockSurfaceHeaderBackground == palette.background)
|
||||
#expect(palette.blockElevatedBackground == palette.background)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ struct PaletteRegistryTests {
|
||||
|
||||
@Test("Registry contains all predefined palettes")
|
||||
func registryCount() {
|
||||
// Green, Amber, Red, Violet, Blue, White, NCurses = 7
|
||||
#expect(PaletteRegistry.all.count == 7)
|
||||
// Green, Amber, Red, Violet, Blue, White = 6
|
||||
#expect(PaletteRegistry.all.count == 6)
|
||||
}
|
||||
|
||||
@Test("Registry cycling order follows color spectrum")
|
||||
@@ -26,7 +26,6 @@ struct PaletteRegistryTests {
|
||||
#expect(PaletteRegistry.all[3].id == "violet")
|
||||
#expect(PaletteRegistry.all[4].id == "blue")
|
||||
#expect(PaletteRegistry.all[5].id == "white")
|
||||
#expect(PaletteRegistry.all[6].id == "ncurses")
|
||||
}
|
||||
|
||||
@Test("Registry finds palette by ID")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// PredefinedPaletteTests.swift
|
||||
// TUIkit
|
||||
//
|
||||
// Tests for all predefined palette structs: Green, Amber, Red, Violet, Blue, White, NCurses.
|
||||
// Tests for all predefined palette structs: Green, Amber, Red, Violet, Blue, White.
|
||||
//
|
||||
|
||||
import Testing
|
||||
@@ -37,36 +37,28 @@ struct GreenPaletteTests {
|
||||
#expect(palette.name == "Green")
|
||||
}
|
||||
|
||||
@Test("Green palette backgrounds get progressively brighter from app to container to buttons")
|
||||
func greenBackgroundLuminanceOrder() throws {
|
||||
@Test("Green palette block surfaces get progressively brighter")
|
||||
func greenBlockSurfaceLuminanceOrder() throws {
|
||||
let palette = GreenPalette()
|
||||
let bgLum = try #require(relativeLuminance(of: palette.background))
|
||||
let bodyLum = try #require(relativeLuminance(of: palette.containerBodyBackground))
|
||||
let capLum = try #require(relativeLuminance(of: palette.containerCapBackground))
|
||||
let buttonLum = try #require(relativeLuminance(of: palette.buttonBackground))
|
||||
let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground))
|
||||
let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground))
|
||||
let elevatedLum = try #require(relativeLuminance(of: palette.elevatedBackground))
|
||||
|
||||
#expect(bgLum < capLum, "background should be darker than containerCapBackground")
|
||||
#expect(bgLum < bodyLum, "background should be darker than containerBodyBackground")
|
||||
#expect(buttonLum > capLum, "buttonBackground should be brighter than containerCapBackground")
|
||||
#expect(bgLum < headerLum, "background should be darker than surfaceHeaderBackground")
|
||||
#expect(bgLum < surfaceLum, "background should be darker than surfaceBackground")
|
||||
#expect(elevatedLum > headerLum, "elevatedBackground should be brighter than surfaceHeaderBackground")
|
||||
}
|
||||
|
||||
@Test("Green palette foregrounds get progressively dimmer including placeholder")
|
||||
@Test("Green palette foregrounds get progressively dimmer")
|
||||
func greenForegroundLuminanceOrder() throws {
|
||||
let palette = GreenPalette()
|
||||
let fgLum = try #require(relativeLuminance(of: palette.foreground))
|
||||
let fgSecLum = try #require(relativeLuminance(of: palette.foregroundSecondary))
|
||||
let fgTerLum = try #require(relativeLuminance(of: palette.foregroundTertiary))
|
||||
let fgPlaceLum = try #require(relativeLuminance(of: palette.foregroundPlaceholder))
|
||||
|
||||
#expect(fgLum > fgSecLum, "foreground should be brighter than foregroundSecondary")
|
||||
#expect(fgSecLum > fgTerLum, "foregroundSecondary should be brighter than foregroundTertiary")
|
||||
#expect(fgTerLum > fgPlaceLum, "foregroundTertiary should be brighter than foregroundPlaceholder")
|
||||
}
|
||||
|
||||
@Test("Green palette has distinct accent colors")
|
||||
func greenAccents() {
|
||||
let palette = GreenPalette()
|
||||
#expect(palette.accent != palette.accentSecondary)
|
||||
}
|
||||
|
||||
@Test("Green palette has all semantic colors")
|
||||
@@ -136,17 +128,17 @@ struct VioletPaletteTests {
|
||||
#expect(palette.name == "Violet")
|
||||
}
|
||||
|
||||
@Test("Violet palette backgrounds get progressively brighter from app to container to buttons")
|
||||
func violetBackgroundLuminanceOrder() throws {
|
||||
@Test("Violet palette block surfaces get progressively brighter")
|
||||
func violetBlockSurfaceLuminanceOrder() throws {
|
||||
let palette = VioletPalette()
|
||||
let bgLum = try #require(relativeLuminance(of: palette.background))
|
||||
let bodyLum = try #require(relativeLuminance(of: palette.containerBodyBackground))
|
||||
let capLum = try #require(relativeLuminance(of: palette.containerCapBackground))
|
||||
let buttonLum = try #require(relativeLuminance(of: palette.buttonBackground))
|
||||
let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground))
|
||||
let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground))
|
||||
let elevatedLum = try #require(relativeLuminance(of: palette.elevatedBackground))
|
||||
|
||||
#expect(bgLum < capLum, "background should be darker than containerCapBackground")
|
||||
#expect(bgLum < bodyLum, "background should be darker than containerBodyBackground")
|
||||
#expect(buttonLum > capLum, "buttonBackground should be brighter than containerCapBackground")
|
||||
#expect(bgLum < headerLum, "background should be darker than surfaceHeaderBackground")
|
||||
#expect(bgLum < surfaceLum, "background should be darker than surfaceBackground")
|
||||
#expect(elevatedLum > headerLum, "elevatedBackground should be brighter than surfaceHeaderBackground")
|
||||
}
|
||||
|
||||
@Test("Violet palette colors differ from green palette")
|
||||
@@ -170,17 +162,17 @@ struct BluePaletteTests {
|
||||
#expect(palette.name == "Blue")
|
||||
}
|
||||
|
||||
@Test("Blue palette backgrounds get progressively brighter from app to container to buttons")
|
||||
func blueBackgroundLuminanceOrder() throws {
|
||||
@Test("Blue palette block surfaces get progressively brighter")
|
||||
func blueBlockSurfaceLuminanceOrder() throws {
|
||||
let palette = BluePalette()
|
||||
let bgLum = try #require(relativeLuminance(of: palette.background))
|
||||
let bodyLum = try #require(relativeLuminance(of: palette.containerBodyBackground))
|
||||
let capLum = try #require(relativeLuminance(of: palette.containerCapBackground))
|
||||
let buttonLum = try #require(relativeLuminance(of: palette.buttonBackground))
|
||||
let headerLum = try #require(relativeLuminance(of: palette.surfaceHeaderBackground))
|
||||
let surfaceLum = try #require(relativeLuminance(of: palette.surfaceBackground))
|
||||
let elevatedLum = try #require(relativeLuminance(of: palette.elevatedBackground))
|
||||
|
||||
#expect(bgLum < capLum, "background should be darker than containerCapBackground")
|
||||
#expect(bgLum < bodyLum, "background should be darker than containerBodyBackground")
|
||||
#expect(buttonLum > capLum, "buttonBackground should be brighter than containerCapBackground")
|
||||
#expect(bgLum < headerLum, "background should be darker than surfaceHeaderBackground")
|
||||
#expect(bgLum < surfaceLum, "background should be darker than surfaceBackground")
|
||||
#expect(elevatedLum > headerLum, "elevatedBackground should be brighter than surfaceHeaderBackground")
|
||||
}
|
||||
|
||||
@Test("Blue palette colors differ from violet palette")
|
||||
@@ -191,28 +183,3 @@ struct BluePaletteTests {
|
||||
#expect(blue.accent != violet.accent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NCurses Palette Tests
|
||||
|
||||
@Suite("NCurses Palette Tests")
|
||||
struct NCursesPaletteTests {
|
||||
|
||||
@Test("NCurses palette has correct identity")
|
||||
func ncursesIdentity() {
|
||||
let palette = NCursesPalette()
|
||||
#expect(palette.id == "ncurses")
|
||||
#expect(palette.name == "ncurses")
|
||||
}
|
||||
|
||||
@Test("NCurses palette uses standard terminal colors")
|
||||
func ncursesUsesStandardColors() {
|
||||
let palette = NCursesPalette()
|
||||
#expect(palette.background == .black)
|
||||
#expect(palette.foreground == .white)
|
||||
#expect(palette.accent == .cyan)
|
||||
#expect(palette.success == .green)
|
||||
#expect(palette.warning == .yellow)
|
||||
#expect(palette.error == .red)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-59
@@ -272,7 +272,7 @@
|
||||
<body>
|
||||
|
||||
<h1>TUIKit Color Palettes</h1>
|
||||
<p class="subtitle">5 new palettes designed for terminal UI aesthetics — plus existing palettes for reference</p>
|
||||
<p class="subtitle">5 proposed palettes with Palette (13) + BlockPalette (3) properties — plus existing palettes for reference</p>
|
||||
|
||||
<!-- Hue distribution visualization -->
|
||||
<div class="hue-wheel">
|
||||
@@ -313,23 +313,21 @@ const newPalettes = [
|
||||
desc: "Inspired by LCARS (Star Trek) terminal displays, teal control panels, and bridge consoles.",
|
||||
colors: {
|
||||
background: "#060A0A",
|
||||
containerBodyBackground: "#0E2526",
|
||||
containerCapBackground: "#0A1B1C",
|
||||
foreground: "#00DDC0",
|
||||
foregroundSecondary: "#00AA94",
|
||||
foregroundTertiary: "#007A6B",
|
||||
foregroundPlaceholder: "#00574C",
|
||||
accent: "#33FFE0",
|
||||
accentSecondary: "#00BB9E",
|
||||
success: "#33FFAA",
|
||||
warning: "#CCFF66",
|
||||
error: "#FF6633",
|
||||
info: "#66FFEE",
|
||||
border: "#2D5A55",
|
||||
statusBarBackground: "#0D1E1D",
|
||||
buttonBackground: "#144542",
|
||||
appHeaderBackground: "#0A1B1C",
|
||||
overlayBackground: "#060A0A",
|
||||
surfaceBackground: "#0E2526",
|
||||
surfaceHeaderBackground: "#0A1B1C",
|
||||
elevatedBackground: "#144542",
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -337,23 +335,21 @@ const newPalettes = [
|
||||
desc: "Synthwave neon glow \u2014 inspired by 80s arcade cabinets, retrowave, and cyberpunk HUDs.",
|
||||
colors: {
|
||||
background: "#0A060A",
|
||||
containerBodyBackground: "#251428",
|
||||
containerCapBackground: "#1C0E1E",
|
||||
foreground: "#FF44CC",
|
||||
foregroundSecondary: "#CC33A3",
|
||||
foregroundTertiary: "#8F2472",
|
||||
foregroundPlaceholder: "#661952",
|
||||
accent: "#FF77DD",
|
||||
accentSecondary: "#DD33AA",
|
||||
success: "#FF88DD",
|
||||
warning: "#FFAA77",
|
||||
error: "#FF6633",
|
||||
info: "#FF99EE",
|
||||
border: "#5A2D50",
|
||||
statusBarBackground: "#180D19",
|
||||
buttonBackground: "#3A1D3A",
|
||||
appHeaderBackground: "#1C0E1E",
|
||||
overlayBackground: "#0A060A",
|
||||
surfaceBackground: "#251428",
|
||||
surfaceHeaderBackground: "#1C0E1E",
|
||||
elevatedBackground: "#3A1D3A",
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -361,23 +357,21 @@ const newPalettes = [
|
||||
desc: "Warm brass instrument panels, steampunk gauges, and luxury cockpit displays.",
|
||||
colors: {
|
||||
background: "#0A0906",
|
||||
containerBodyBackground: "#261F0E",
|
||||
containerCapBackground: "#1C170A",
|
||||
foreground: "#DDAA22",
|
||||
foregroundSecondary: "#BB8E1A",
|
||||
foregroundTertiary: "#8A6814",
|
||||
foregroundPlaceholder: "#634B0E",
|
||||
accent: "#FFCC44",
|
||||
accentSecondary: "#CC9922",
|
||||
success: "#DDCC44",
|
||||
warning: "#FFE066",
|
||||
error: "#FF6633",
|
||||
info: "#EEBB55",
|
||||
border: "#5A4D2D",
|
||||
statusBarBackground: "#191508",
|
||||
buttonBackground: "#3A3018",
|
||||
appHeaderBackground: "#1C170A",
|
||||
overlayBackground: "#0A0906",
|
||||
surfaceBackground: "#261F0E",
|
||||
surfaceHeaderBackground: "#1C170A",
|
||||
elevatedBackground: "#3A3018",
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -385,23 +379,21 @@ const newPalettes = [
|
||||
desc: "Warm sunset terminals \u2014 inspired by sodium-vapor streetlights and desert radar stations.",
|
||||
colors: {
|
||||
background: "#0A0706",
|
||||
containerBodyBackground: "#281710",
|
||||
containerCapBackground: "#1E100C",
|
||||
foreground: "#FF7744",
|
||||
foregroundSecondary: "#CC5E36",
|
||||
foregroundTertiary: "#8F4226",
|
||||
foregroundPlaceholder: "#66301B",
|
||||
accent: "#FF9966",
|
||||
accentSecondary: "#DD6633",
|
||||
success: "#FFAA77",
|
||||
warning: "#FFCC66",
|
||||
error: "#FF4433",
|
||||
info: "#FFBB88",
|
||||
border: "#5A3A2D",
|
||||
statusBarBackground: "#190F0A",
|
||||
buttonBackground: "#3A2418",
|
||||
appHeaderBackground: "#1E100C",
|
||||
overlayBackground: "#0A0706",
|
||||
surfaceBackground: "#281710",
|
||||
surfaceHeaderBackground: "#1E100C",
|
||||
elevatedBackground: "#3A2418",
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -409,23 +401,21 @@ const newPalettes = [
|
||||
desc: "Arctic cryogenic readouts \u2014 inspired by ice-blue instrument clusters and polar research stations.",
|
||||
colors: {
|
||||
background: "#060809",
|
||||
containerBodyBackground: "#0E1D28",
|
||||
containerCapBackground: "#0A151E",
|
||||
foreground: "#88DDFF",
|
||||
foregroundSecondary: "#66AACC",
|
||||
foregroundTertiary: "#4A7A99",
|
||||
foregroundPlaceholder: "#35586E",
|
||||
accent: "#AAEEFF",
|
||||
accentSecondary: "#66BBDD",
|
||||
success: "#88EEBB",
|
||||
warning: "#CCDDAA",
|
||||
error: "#FF6633",
|
||||
info: "#BBEEFF",
|
||||
border: "#2D4A5A",
|
||||
statusBarBackground: "#0D1720",
|
||||
buttonBackground: "#142D3E",
|
||||
appHeaderBackground: "#0A151E",
|
||||
overlayBackground: "#060809",
|
||||
surfaceBackground: "#0E1D28",
|
||||
surfaceHeaderBackground: "#0A151E",
|
||||
elevatedBackground: "#142D3E",
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -435,85 +425,87 @@ const existingPalettes = [
|
||||
name: "Green",
|
||||
desc: "P1 phosphor",
|
||||
fg: "#33FF33",
|
||||
colors: ["#060A07","#0E271C","#0A1B13",
|
||||
"#33FF33","#27C227","#1F8F1F","#165A16",
|
||||
"#66FF66","#00CC00",
|
||||
colors: ["#060A07",
|
||||
"#33FF33","#27C227","#1F8F1F",
|
||||
"#66FF66",
|
||||
"#33FF33","#CCFF33","#FF6633","#33FFCC",
|
||||
"#2D5A2D",
|
||||
"#0F2215","#145523","#0A1B13","#060A07"]
|
||||
"#0F2215","#0A1B13","#060A07"]
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
desc: "P3 phosphor",
|
||||
fg: "#FFAA00",
|
||||
colors: ["#0A0706","#251710","#1E110E",
|
||||
"#FFAA00","#CC8800","#8F6600","#664D00",
|
||||
"#FFCC33","#CC9900",
|
||||
colors: ["#0A0706",
|
||||
"#FFAA00","#CC8800","#8F6600",
|
||||
"#FFCC33",
|
||||
"#FFCC00","#FFE066","#FF6633","#FFD966",
|
||||
"#5A4A2D",
|
||||
"#191613","#3A2A1D","#1E110E","#0A0706"]
|
||||
"#191613","#1E110E","#0A0706"]
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
desc: "Military",
|
||||
fg: "#FF4444",
|
||||
colors: ["#0A0606","#281112","#1E0F10",
|
||||
"#FF4444","#CC3333","#8F2222","#661616",
|
||||
"#FF6666","#CC4444",
|
||||
colors: ["#0A0606",
|
||||
"#FF4444","#CC3333","#8F2222",
|
||||
"#FF6666",
|
||||
"#FF8080","#FFAA66","#FFFFFF","#FF9999",
|
||||
"#5A2D2D",
|
||||
"#191313","#3A1F22","#1E0F10","#0A0606"]
|
||||
"#191313","#1E0F10","#0A0606"]
|
||||
},
|
||||
{
|
||||
name: "Violet",
|
||||
desc: "Sci-fi",
|
||||
fg: "#B275EF",
|
||||
colors: ["#070509","#190F23","#110B18",
|
||||
"#B275EF","#8C3BDC","#6528A3","#47236B",
|
||||
"#C697F6","#7F1FDF",
|
||||
colors: ["#070509",
|
||||
"#B275EF","#8C3BDC","#6528A3",
|
||||
"#C697F6",
|
||||
"#E4A567","#EF75B2","#A5F159","#7CB2E8",
|
||||
"#3F2659",
|
||||
"#140D1B","#261537","#110B18","#070509"]
|
||||
"#140D1B","#110B18","#070509"]
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
desc: "VFD",
|
||||
fg: "#00AAFF",
|
||||
colors: ["#060708","#0E1825","#0A121C",
|
||||
"#00AAFF","#0088CC","#006699","#004D73",
|
||||
"#33BBFF","#0099DD",
|
||||
colors: ["#060708",
|
||||
"#00AAFF","#0088CC","#006699",
|
||||
"#33BBFF",
|
||||
"#33CCFF","#66CCFF","#FF6633","#99DDFF",
|
||||
"#2D4A5A",
|
||||
"#0F1822","#14304A","#0A121C","#060708"]
|
||||
"#0F1822","#0A121C","#060708"]
|
||||
},
|
||||
{
|
||||
name: "White",
|
||||
desc: "P4 phosphor",
|
||||
fg: "#E8E8E8",
|
||||
colors: ["#06070A","#111A2A","#0D131D",
|
||||
"#E8E8E8","#B0B0B0","#787878","#505050",
|
||||
"#FFFFFF","#C0C0C0",
|
||||
colors: ["#06070A",
|
||||
"#E8E8E8","#B0B0B0","#787878",
|
||||
"#FFFFFF",
|
||||
"#C0FFC0","#FFE0A0","#FFA0A0","#A0D0FF",
|
||||
"#484848",
|
||||
"#131619","#1D2535","#0D131D","#06070A"]
|
||||
"#131619","#0D131D","#06070A"]
|
||||
},
|
||||
];
|
||||
|
||||
const colorLabels = [
|
||||
"background","containerBodyBackground","containerCapBackground",
|
||||
"foreground","foregroundSecondary","foregroundTertiary","foregroundPlaceholder",
|
||||
"accent","accentSecondary",
|
||||
"background",
|
||||
"foreground","foregroundSecondary","foregroundTertiary",
|
||||
"accent",
|
||||
"success","warning","error","info",
|
||||
"border",
|
||||
"statusBarBackground","buttonBackground","appHeaderBackground","overlayBackground"
|
||||
"statusBarBackground","appHeaderBackground","overlayBackground",
|
||||
"surfaceBackground","surfaceHeaderBackground","elevatedBackground"
|
||||
];
|
||||
|
||||
const swatchGroups = [
|
||||
{ title: "Backgrounds", keys: ["background","containerBodyBackground","containerCapBackground","buttonBackground","statusBarBackground","appHeaderBackground","overlayBackground"] },
|
||||
{ title: "Foregrounds", keys: ["foreground","foregroundSecondary","foregroundTertiary","foregroundPlaceholder"] },
|
||||
{ title: "Accents", keys: ["accent","accentSecondary"] },
|
||||
{ title: "Semantic", keys: ["success","warning","error","info"] },
|
||||
{ title: "UI Elements", keys: ["border"] },
|
||||
{ title: "Palette — Backgrounds", keys: ["background","statusBarBackground","appHeaderBackground","overlayBackground"] },
|
||||
{ title: "Palette — Foregrounds", keys: ["foreground","foregroundSecondary","foregroundTertiary"] },
|
||||
{ title: "Palette — Accent", keys: ["accent"] },
|
||||
{ title: "Palette — Semantic", keys: ["success","warning","error","info"] },
|
||||
{ title: "Palette — UI Elements", keys: ["border"] },
|
||||
{ title: "BlockPalette — Surfaces", keys: ["surfaceBackground","surfaceHeaderBackground","elevatedBackground"] },
|
||||
];
|
||||
|
||||
function camelToLabel(key) {
|
||||
|
||||
Reference in New Issue
Block a user