Files
TUIkit/Sources/TUIKit/Views/Panel.swift
T
phranck 3470cb8e24 refactor: Use theme colors throughout UI components
- Update all 4 phosphor themes (Green, Amber, White, Red) to use neutral
  dark background (#1E1E1E) matching Spotnik reference
- Simplify theme names (remove 'Phosphor' suffix)
- Menu: Use theme.foreground for items, theme.accent for selection
- Button: Use theme colors for default focused style and focus indicator
- Panel/Card: Use theme.border as default border color
- BorderModifier: Use theme.border as default

All 189 tests passing.
2026-01-28 21:08:25 +01:00

152 lines
4.9 KiB
Swift

//
// Panel.swift
// TUIKit
//
// A titled container view with a header.
//
/// A bordered container with a title in the top border.
///
/// `Panel` is useful for grouping content with a visible label,
/// similar to a fieldset in HTML or a group box in desktop UIs.
///
/// # Example
///
/// ```swift
/// Panel("Settings") {
/// Text("Option 1")
/// Text("Option 2")
/// }
///
/// Panel("User Info", borderStyle: .doubleLine, titleColor: .cyan) {
/// Text("Name: John")
/// Text("Age: 30")
/// }
/// ```
public struct Panel<Content: View>: View {
/// The title displayed in the top border.
public let title: String
/// The content of the panel.
public let content: Content
/// The border style.
public let borderStyle: BorderStyle
/// The border color.
public let borderColor: Color?
/// The title color.
public let titleColor: Color?
/// The padding inside the panel.
public let padding: EdgeInsets
/// Creates a panel with the specified options.
///
/// - Parameters:
/// - title: The title to display in the top border.
/// - borderStyle: The border style (default: .line).
/// - borderColor: The border color (default: nil).
/// - titleColor: The title color (default: nil, same as border).
/// - padding: The inner padding (default: horizontal 1, vertical 0).
/// - content: The content of the panel.
public init(
_ title: String,
borderStyle: BorderStyle = .line,
borderColor: Color? = nil,
titleColor: Color? = nil,
padding: EdgeInsets = EdgeInsets(horizontal: 1, vertical: 0),
@ViewBuilder content: () -> Content
) {
self.title = title
self.content = content()
self.borderStyle = borderStyle
self.borderColor = borderColor
self.titleColor = titleColor
self.padding = padding
}
public var body: Never {
fatalError("Panel renders via Renderable")
}
}
// MARK: - Panel Rendering
extension Panel: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
// Render the content first
let paddedContent = content.padding(padding)
let contentBuffer = TUIKit.renderToBuffer(paddedContent, context: context)
guard !contentBuffer.isEmpty else {
return FrameBuffer()
}
// Title with spaces: " Title "
let titleText = " \(title) "
let titleLength = titleText.count
// Inner width must fit both content and title (plus corner + one horizontal on each side)
// Top line structure: ┌─ Title ─────┐
// So minimum inner width = titleLength + 2 (for the ─ on each side of title)
let innerWidth = max(contentBuffer.width, titleLength + 2)
// Build top border with title
// Format: ┌─ Title ─────┐
let titleStyled = colorize(titleText, with: titleColor ?? borderColor)
// Left part: corner + one horizontal
let leftPart = colorize(
String(borderStyle.topLeft) + String(borderStyle.horizontal),
with: borderColor
)
// Right part: remaining horizontals + corner
// Total top line width (excluding corners) = innerWidth
// Used by: 1 (left horizontal) + titleLength + rightPartLength = innerWidth
let rightPartLength = max(0, innerWidth - 1 - titleLength)
let rightPart = colorize(
String(repeating: borderStyle.horizontal, count: rightPartLength) + String(borderStyle.topRight),
with: borderColor
)
let topLine = leftPart + titleStyled + rightPart
// Build bottom border (innerWidth horizontals between corners)
let bottomLine = colorize(
String(borderStyle.bottomLeft)
+ String(repeating: borderStyle.horizontal, count: innerWidth)
+ String(borderStyle.bottomRight),
with: borderColor
)
// Build result
var lines: [String] = []
lines.append(topLine)
// Content lines with side borders
// Important: Add reset before right border to prevent color bleeding
let reset = "\u{1B}[0m"
let leftBorder = colorize(String(borderStyle.vertical), with: borderColor)
let rightBorder = colorize(String(borderStyle.vertical), with: borderColor)
for line in contentBuffer.lines {
let paddedLine = line.padToVisibleWidth(innerWidth)
lines.append(leftBorder + paddedLine + reset + rightBorder)
}
lines.append(bottomLine)
return FrameBuffer(lines: lines)
}
/// Applies color to a string, using theme border color as default.
private func colorize(_ string: String, with color: Color?) -> String {
var style = TextStyle()
style.foregroundColor = color ?? Color.theme.border
return ANSIRenderer.render(string, with: style)
}
}