mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Refactor: HSL-based color system for all palettes — lighter/darker preserve hue, unified baseHue architecture
This commit is contained in:
@@ -347,25 +347,68 @@ public struct Color: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjusts brightness by a signed amount.
|
||||
/// Adjusts a color's lightness by the given amount in HSL space.
|
||||
///
|
||||
/// 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.
|
||||
/// Positive values lighten, negative values darken. Converts to HSL first,
|
||||
/// adjusts only the lightness component, then converts back to RGB.
|
||||
/// This preserves hue and saturation, preventing colors from shifting
|
||||
/// toward gray when lightened or darkened.
|
||||
///
|
||||
/// - Parameter amount: The adjustment amount (-1 to 1).
|
||||
/// - Parameter amount: The lightness adjustment (-1 to 1).
|
||||
/// - Returns: The adjusted color as RGB, or self if semantic (unresolved).
|
||||
private func adjusted(by amount: Double) -> Self {
|
||||
guard let (red, green, blue) = rgbComponents else {
|
||||
return self
|
||||
}
|
||||
|
||||
let shift = 255 * amount
|
||||
let newRed = UInt8(min(255, max(0, Double(red) + shift)))
|
||||
let newGreen = UInt8(min(255, max(0, Double(green) + shift)))
|
||||
let newBlue = UInt8(min(255, max(0, Double(blue) + shift)))
|
||||
let (hue, saturation, lightness) = Self.rgbToHSL(red: red, green: green, blue: blue)
|
||||
let newLightness = min(100, max(0, lightness + amount * 100))
|
||||
|
||||
return .rgb(newRed, newGreen, newBlue)
|
||||
return .hsl(hue, saturation, newLightness)
|
||||
}
|
||||
|
||||
/// Converts RGB components to HSL (hue 0–360, saturation 0–100, lightness 0–100).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - red: Red component (0–255).
|
||||
/// - green: Green component (0–255).
|
||||
/// - blue: Blue component (0–255).
|
||||
/// - Returns: A tuple of (hue, saturation, lightness) in their standard ranges.
|
||||
static func rgbToHSL(red: UInt8, green: UInt8, blue: UInt8) -> (hue: Double, saturation: Double, lightness: Double) {
|
||||
let normalizedRed = Double(red) / 255.0
|
||||
let normalizedGreen = Double(green) / 255.0
|
||||
let normalizedBlue = Double(blue) / 255.0
|
||||
|
||||
let maxComponent = max(normalizedRed, normalizedGreen, normalizedBlue)
|
||||
let minComponent = min(normalizedRed, normalizedGreen, normalizedBlue)
|
||||
let delta = maxComponent - minComponent
|
||||
|
||||
let lightness = (maxComponent + minComponent) / 2.0
|
||||
|
||||
guard delta > 0 else {
|
||||
// Achromatic (gray)
|
||||
return (hue: 0, saturation: 0, lightness: lightness * 100)
|
||||
}
|
||||
|
||||
let saturation: Double
|
||||
if lightness < 0.5 {
|
||||
saturation = delta / (maxComponent + minComponent)
|
||||
} else {
|
||||
saturation = delta / (2.0 - maxComponent - minComponent)
|
||||
}
|
||||
|
||||
let hue: Double
|
||||
switch maxComponent {
|
||||
case normalizedRed:
|
||||
let segment = (normalizedGreen - normalizedBlue) / delta
|
||||
hue = 60 * (segment < 0 ? segment + 6 : segment)
|
||||
case normalizedGreen:
|
||||
hue = 60 * ((normalizedBlue - normalizedRed) / delta + 2)
|
||||
default:
|
||||
hue = 60 * ((normalizedRed - normalizedGreen) / delta + 4)
|
||||
}
|
||||
|
||||
return (hue: hue, saturation: saturation * 100, lightness: lightness * 100)
|
||||
}
|
||||
|
||||
/// Returns a color with adjusted opacity (simulated via color mixing).
|
||||
|
||||
@@ -8,37 +8,75 @@
|
||||
/// Classic amber terminal palette (P3 phosphor).
|
||||
///
|
||||
/// Inspired by terminals like the IBM 3278 and Wyse 50.
|
||||
/// Uses a dark background with subtle amber/orange tint.
|
||||
/// 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"
|
||||
|
||||
// Background
|
||||
public let background = Color.hex(0x0A0706)
|
||||
/// The base hue used to generate all palette colors.
|
||||
private static let baseHue: Double = 40
|
||||
|
||||
// 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
|
||||
// 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.hex(0xFFCC33) // Lighter amber for highlights
|
||||
public let accent: Color
|
||||
|
||||
// Semantic colors (stay in amber family)
|
||||
public let success = Color.hex(0xFFCC00)
|
||||
public let warning = Color.hex(0xFFE066) // Light amber
|
||||
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
|
||||
public let info = Color.hex(0xFFD966) // Light amber
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x5A4A2D) // Subtle amber border
|
||||
public let border: Color
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x191613)
|
||||
public let appHeaderBackground = Color.hex(0x1E110E)
|
||||
public let overlayBackground = Color.hex(0x0A0706)
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
|
||||
public init() {}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -7,38 +7,76 @@
|
||||
/// Blue VFD terminal palette.
|
||||
///
|
||||
/// 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.
|
||||
/// 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.hex(0x060708)
|
||||
public let background: Color
|
||||
|
||||
// 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 foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accent
|
||||
public let accent = Color.hex(0x33BBFF) // Lighter blue for highlights
|
||||
public let accent: Color
|
||||
|
||||
// Semantic colors (stay in blue family)
|
||||
public let success = Color.hex(0x33CCFF) // Cyan-blue
|
||||
public let warning = Color.hex(0x66CCFF) // Light cyan
|
||||
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
|
||||
public let info = Color.hex(0x99DDFF) // Pale blue
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x2D4A5A) // Subtle blue border
|
||||
public let border: Color
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x0F1822)
|
||||
public let appHeaderBackground = Color.hex(0x0A121C)
|
||||
public let overlayBackground = Color.hex(0x060708)
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
|
||||
public init() {}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -8,37 +8,75 @@
|
||||
/// Classic green terminal palette (P1 phosphor).
|
||||
///
|
||||
/// Inspired by early CRT monitors like the IBM 5151 and Apple II.
|
||||
/// Uses a dark background with subtle green tint.
|
||||
/// 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.hex(0x060A07)
|
||||
public let background: Color
|
||||
|
||||
// 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 foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accent
|
||||
public let accent = Color.hex(0x66FF66) // Lighter green for highlights
|
||||
public let accent: Color
|
||||
|
||||
// Semantic colors (stay in green family)
|
||||
public let success = Color.hex(0x33FF33)
|
||||
public let warning = Color.hex(0xCCFF33) // Yellow-green
|
||||
public let error = Color.hex(0xFF6633) // Orange-red (contrast)
|
||||
public let info = Color.hex(0x33FFCC) // Cyan-green
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x2D5A2D) // Subtle green border
|
||||
public let border: Color
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x0F2215)
|
||||
public let appHeaderBackground = Color.hex(0x0A1B13)
|
||||
public let overlayBackground = Color.hex(0x060A07)
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
|
||||
public init() {}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -9,36 +9,75 @@
|
||||
///
|
||||
/// 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.hex(0x0A0606)
|
||||
public let background: Color
|
||||
|
||||
// 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 foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accent
|
||||
public let accent = Color.hex(0xFF6666) // Lighter red for highlights
|
||||
public let accent: Color
|
||||
|
||||
// Semantic colors (stay in red family)
|
||||
public let success = Color.hex(0xFF8080) // Light red (success in red theme)
|
||||
public let warning = Color.hex(0xFFAA66) // Orange
|
||||
public let error = Color.hex(0xFFFFFF) // White (stands out as error)
|
||||
public let info = Color.hex(0xFF9999) // Light red
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x5A2D2D) // Subtle red border
|
||||
public let border: Color
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x191313)
|
||||
public let appHeaderBackground = Color.hex(0x1E0F10)
|
||||
public let overlayBackground = Color.hex(0x0A0606)
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
|
||||
public init() {}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -65,7 +65,7 @@ public struct VioletPalette: BlockPalette {
|
||||
self.border = Color.hsl(hue, 40, 25)
|
||||
|
||||
// Additional backgrounds
|
||||
self.statusBarBackground = Color.hsl(hue, 35, 8)
|
||||
self.statusBarBackground = Color.hsl(hue, 35, 10)
|
||||
self.appHeaderBackground = Color.hsl(hue, 35, 7)
|
||||
self.overlayBackground = Color.hsl(hue, 30, 3)
|
||||
}
|
||||
|
||||
@@ -8,37 +8,69 @@
|
||||
/// Classic white terminal palette (P4 phosphor).
|
||||
///
|
||||
/// Inspired by terminals like the DEC VT100 and VT220.
|
||||
/// Uses a dark background with subtle cool/blue tint.
|
||||
/// 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.hex(0x06070A)
|
||||
public let background: Color
|
||||
|
||||
// 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 foreground: Color
|
||||
public let foregroundSecondary: Color
|
||||
public let foregroundTertiary: Color
|
||||
|
||||
// Accent
|
||||
public let accent = Color.hex(0xFFFFFF) // Pure white for highlights
|
||||
public let accent: Color
|
||||
|
||||
// Semantic colors (subtle tints)
|
||||
public let success = Color.hex(0xC0FFC0) // Slight green tint
|
||||
public let warning = Color.hex(0xFFE0A0) // Slight amber tint
|
||||
public let error = Color.hex(0xFFA0A0) // Slight red tint
|
||||
public let info = Color.hex(0xA0D0FF) // Slight blue tint
|
||||
// Semantic colors
|
||||
public let success: Color
|
||||
public let warning: Color
|
||||
public let error: Color
|
||||
public let info: Color
|
||||
|
||||
// UI elements
|
||||
public let border = Color.hex(0x484848) // Subtle gray border
|
||||
public let border: Color
|
||||
|
||||
// Additional backgrounds
|
||||
public let statusBarBackground = Color.hex(0x131619)
|
||||
public let appHeaderBackground = Color.hex(0x0D131D)
|
||||
public let overlayBackground = Color.hex(0x06070A)
|
||||
public let statusBarBackground: Color
|
||||
public let appHeaderBackground: Color
|
||||
public let overlayBackground: Color
|
||||
|
||||
public init() {}
|
||||
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
|
||||
|
||||
@@ -138,8 +138,8 @@ public protocol BlockPalette: Palette {
|
||||
// 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 surfaceBackground: Color { background.lighter(by: 0.10) }
|
||||
public var surfaceHeaderBackground: Color { background.lighter(by: 0.07) }
|
||||
public var elevatedBackground: Color { surfaceHeaderBackground.lighter(by: 0.05) }
|
||||
}
|
||||
|
||||
|
||||
@@ -292,20 +292,30 @@ extension ContainerView: Renderable {
|
||||
let indicatorColor = context.focusIndicatorColor
|
||||
innerContext.focusIndicatorColor = nil
|
||||
|
||||
// Render body content first to determine its width.
|
||||
// Render body content first to determine its natural width.
|
||||
let paddedContent = content.padding(padding)
|
||||
let bodyBuffer = TUIkit.renderToBuffer(paddedContent, context: innerContext)
|
||||
|
||||
// Calculate inner width from body and title (footer adapts to this).
|
||||
let titleWidth = title.map { $0.count + 4 } ?? 0 // " Title " + borders
|
||||
let innerWidth = max(titleWidth, bodyBuffer.width)
|
||||
|
||||
// Render footer constrained to the actual inner width.
|
||||
// PaddingModifier is post-processing (doesn't reduce availableWidth for
|
||||
// its child), so we subtract the footer padding from the context width
|
||||
// manually. This ensures Spacer() in the footer fills exactly the
|
||||
// container's inner width.
|
||||
// Render footer with full available width for initial measurement.
|
||||
// This ensures the footer's natural width is included in the
|
||||
// innerWidth calculation, preventing truncation when footer content
|
||||
// (e.g. HStack with Spacer + Button) is wider than the body.
|
||||
let footerPadding = EdgeInsets(horizontal: 1, vertical: 0)
|
||||
let initialFooterBuffer: FrameBuffer?
|
||||
if let footerView = footer {
|
||||
let paddedFooter = footerView.padding(footerPadding)
|
||||
initialFooterBuffer = TUIkit.renderToBuffer(paddedFooter, context: innerContext)
|
||||
} else {
|
||||
initialFooterBuffer = nil
|
||||
}
|
||||
|
||||
// Calculate inner width from title, body, AND footer.
|
||||
let titleWidth = title.map { $0.count + 4 } ?? 0 // " Title " + borders
|
||||
let footerNaturalWidth = initialFooterBuffer?.width ?? 0
|
||||
let innerWidth = max(titleWidth, bodyBuffer.width, footerNaturalWidth)
|
||||
|
||||
// Re-render footer constrained to the final innerWidth so that
|
||||
// Spacer() fills exactly the container's inner width.
|
||||
let footerBuffer: FrameBuffer?
|
||||
if let footerView = footer {
|
||||
var footerContext = innerContext
|
||||
|
||||
@@ -19,6 +19,7 @@ enum DemoPage: Int, CaseIterable {
|
||||
case layout
|
||||
case buttons
|
||||
case spinners
|
||||
case blockTheme
|
||||
}
|
||||
|
||||
// MARK: - Content View (Page Router)
|
||||
@@ -63,7 +64,7 @@ struct ContentView: View {
|
||||
.statusBarItems {
|
||||
StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav")
|
||||
StatusBarItem(shortcut: Shortcut.enter, label: "select", key: .enter)
|
||||
StatusBarItem(shortcut: Shortcut.range("1", "7"), label: "jump")
|
||||
StatusBarItem(shortcut: Shortcut.range("1", "8"), label: "jump")
|
||||
}
|
||||
case .textStyles:
|
||||
TextStylesPage()
|
||||
@@ -85,6 +86,9 @@ struct ContentView: View {
|
||||
case .spinners:
|
||||
SpinnersPage()
|
||||
.statusBarItems(subPageItems(pageSetter: pageSetter))
|
||||
case .blockTheme:
|
||||
BlockThemePage()
|
||||
.statusBarItems(subPageItems(pageSetter: pageSetter))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// BlockThemePage.swift
|
||||
// TUIkitExample
|
||||
//
|
||||
// Visual test page for Block appearance background colors.
|
||||
// Shows all background roles side by side for color tuning.
|
||||
//
|
||||
|
||||
import TUIkit
|
||||
|
||||
/// Block theme color tuning page.
|
||||
///
|
||||
/// Displays all background color roles used in block appearance
|
||||
/// as labeled color swatches. This page is designed to be viewed
|
||||
/// exclusively in block appearance mode for visual comparison.
|
||||
struct BlockThemePage: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 1) {
|
||||
HeaderView(title: "Block Theme Colors")
|
||||
|
||||
Text("Switch to block appearance (press 'a') and violet theme (press 't').")
|
||||
.foregroundColor(.palette.foregroundSecondary)
|
||||
|
||||
// Panel with Header, Body, and Footer — shows all 3 block background roles
|
||||
DemoSection("Panel with Header + Body + Footer") {
|
||||
Panel("Header — surfaceHeaderBackground", titleColor: .palette.accent) {
|
||||
Text("Body area — surfaceBackground").foregroundColor(.palette.foreground)
|
||||
Text("Secondary on body").foregroundColor(.palette.foregroundSecondary)
|
||||
Text("Tertiary on body").foregroundColor(.palette.foregroundTertiary)
|
||||
} footer: {
|
||||
Text("Footer — surfaceHeaderBackground").foregroundColor(.palette.foreground)
|
||||
}
|
||||
}
|
||||
|
||||
// Side-by-side containers to compare
|
||||
DemoSection("Panel vs Card vs Box") {
|
||||
HStack(spacing: 2) {
|
||||
Panel("Panel", titleColor: .palette.accent) {
|
||||
Text("Foreground").foregroundColor(.palette.foreground)
|
||||
Text("Secondary").foregroundColor(.palette.foregroundSecondary)
|
||||
Text("Tertiary").foregroundColor(.palette.foregroundTertiary)
|
||||
}
|
||||
|
||||
Card {
|
||||
Text("Foreground").foregroundColor(.palette.foreground)
|
||||
Text("Secondary").foregroundColor(.palette.foregroundSecondary)
|
||||
Text("Tertiary").foregroundColor(.palette.foregroundTertiary)
|
||||
}
|
||||
|
||||
Box {
|
||||
Text("Foreground").foregroundColor(.palette.foreground)
|
||||
Text("Secondary").foregroundColor(.palette.foregroundSecondary)
|
||||
Text("Tertiary").foregroundColor(.palette.foregroundTertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons on app background
|
||||
DemoSection("Buttons — elevatedBackground") {
|
||||
HStack(spacing: 2) {
|
||||
Button("Default") {}
|
||||
Button("Primary", style: .primary) {}
|
||||
Button("Destructive", style: .destructive) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons inside a Panel (on surfaceBackground)
|
||||
DemoSection("Buttons inside Panel") {
|
||||
Panel("Panel with Buttons", titleColor: .palette.accent) {
|
||||
Text("Buttons on surfaceBackground:").foregroundColor(.palette.foregroundSecondary)
|
||||
HStack(spacing: 2) {
|
||||
Button("Default") {}
|
||||
Button("Primary", style: .primary) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ struct MainMenuPage: View {
|
||||
MenuItem(label: "Layout System", shortcut: "5"),
|
||||
MenuItem(label: "Buttons & Focus", shortcut: "6"),
|
||||
MenuItem(label: "Spinners", shortcut: "7"),
|
||||
MenuItem(label: "Block Theme Colors", shortcut: "8"),
|
||||
],
|
||||
selection: $menuSelection,
|
||||
onSelect: { index in
|
||||
|
||||
Reference in New Issue
Block a user