Refactor: HSL-based color system for all palettes — lighter/darker preserve hue, unified baseHue architecture

This commit is contained in:
phranck
2026-02-03 18:11:39 +01:00
parent dc6e3b6437
commit db03e4e41c
12 changed files with 430 additions and 106 deletions
+53 -10
View File
@@ -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 0360, saturation 0100, lightness 0100).
///
/// - Parameters:
/// - red: Red component (0255).
/// - green: Green component (0255).
/// - blue: Blue component (0255).
/// - 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 0360 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 0360 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 0360 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 0360 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
+2 -2
View File
@@ -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) }
}
+20 -10
View File
@@ -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
+5 -1
View File
@@ -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